Conflicts:
	config.json
	init.js
	libs/logUtil.js
This commit is contained in:
Andrea 2014-03-14 19:05:55 +00:00
commit 437e242a25
17 changed files with 614 additions and 114 deletions

View File

@ -34,11 +34,14 @@ of this software. The switching can be controlled using a coin profitability API
#### Community
#### Community / Support
For support and general discussion join IRC #nomp: https://webchat.freenode.net/?channels=#nomp
For development discussion join #nomp-dev: https://webchat.freenode.net/?channels=#nomp-dev
*Having problems getting the portal running due to some module dependency error?* It's probably because you
didn't follow the instructions in this README. Please __read the usage instructions__ including [requirements](#requirements) and [downloading/installing](#1-downloading--installing). If you've followed the instructions completely and are still having problems then open an issue here on github or join our #nomp IRC channel and explain your problem :).
If your pool uses NOMP let us know and we will list your website here.
@ -47,12 +50,12 @@ Usage
#### Requirements
* Coin daemon(s)
* [Node.js](http://nodejs.org/) v0.10+
* [Redis](http://redis.io/) key-value store/database v2.6+
* Coin daemon(s) (find the coin's repo and build latest version from source)
* [Node.js](http://nodejs.org/) v0.10+ ([follow these installation instructions](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager))
* [Redis](http://redis.io/) key-value store v2.6+ ([follow these instructions](http://redis.io/topics/quickstart))
#### 1) Download
#### 1) Downloading & Installing
Clone the repository and run `npm update` for all the dependencies to be installed:
@ -61,19 +64,47 @@ git clone https://github.com/zone117x/node-stratum-portal.git
npm update
```
#### 2) Setup
#### 2) Configuration
##### Portal config
Inside the `config.json` file, ensure the default configuration will work for your environment. The `clustering.forks`
option is set to `"auto"` by default which will spawn one process/fork/worker for each CPU core in your system.
Each of these workers will run a separate instance of your pool(s), and the kernel will load balance miners
using these forks. Optionally, the `clustering.forks` field can be a number for how many forks you wish to spawn.
Inside the `config.json` file, ensure the default configuration will work for your environment.
With `blockNotifyListener` enabled, the master process will start listening on the configured port for messages from
the `scripts/blockNotify.js` script which your coin daemons can be configured to run when a new block is available.
When a blocknotify message is received, the master process uses IPC (inter-process communication) to notify each
worker process about the message. Each worker process then sends the message to the appropriate coin pool.
See "Setting up blocknotify" below to set up your daemon to use this feature.
Explanation for each field:
````javascript
{
/* Specifies the level of log output verbosity. Anything more severy than the level specified
will also be logged. */
"logLevel": "debug", //or "warning", "error"
/* By default 'forks' is set to "auto" which will spawn one process/fork/worker for each CPU
core in your system. Each of these workers will run a separate instance of your pool(s),
and the kernel will load balance miners using these forks. Optionally, the 'forks' field
can be a number for how many forks will be spawned. */
"clustering": {
"enabled": true,
"forks": "auto"
},
/* With this enabled, the master process will start listening on the configured port for
messages from the 'scripts/blockNotify.js' script which your coin daemons can be configured
to run when a new block is available. When a blocknotify message is received, the master
process uses IPC (inter-process communication) to notify each worker process about the
message. Each worker process then sends the message to the appropriate coin pool. See
"Setting up blocknotify" below to set up your daemon to use this feature. */
"blockNotifyListener": {
"enabled": true,
"port": 8117,
"password": "test"
},
/* This is the front-end. Its not finished. When it is finished, this comment will say so. */
"website": {
"enabled": true,
"port": 80,
"liveStats": true
}
}
````
##### Coin config
@ -128,15 +159,16 @@ Description of options:
payments less frequently (they dislike). Opposite for a lower minimum payment. */
"minimumPayment": 0.001,
/* Minimum number of coins to keep in pool wallet. It is recommended to deposit at
at least this many coins into the pool wallet when first starting the pool. */
"minimumReserve": 10,
/* (2% default) What percent fee your pool takes from the block reward. */
"feePercent": 0.02,
/* Your address that receives pool revenue from fees */
"feeReceiveAddress": "LZz44iyF4zLCXJTU8RxztyyJZBntdS6fvv",
/* Minimum number of coins to keep in pool wallet */
"minimumReserve": 10,
/* How many coins from fee revenue must accumulate on top of the minimum reserve amount
in order to trigger withdrawal to fee address. The higher this threshold, the less of
your profit goes to transactions fees. */
@ -286,8 +318,13 @@ blocknotify="scripts/blockNotify.js localhost:8117 mySuperSecurePassword dogecoi
node init.js
```
Optionally, use something like [forever](https://github.com/nodejitsu/forever) to keep the node script running
###### Optional enhancements for your awesome new mining pool server setup:
* Use something like [forever](https://github.com/nodejitsu/forever) to keep the node script running
in case the master process crashes.
* Use something like [redis-commander](https://github.com/joeferner/redis-commander) to have a nice GUI
for exploring your redis database.
* Use something like [logrotator](http://www.thegeekstuff.com/2010/07/logrotate-examples/) to rotate log
output from NOMP.
Donations

View File

@ -1,4 +1,5 @@
{
"logLevel": "debug",
"clustering": {
"enabled": true,
"forks": "1"
@ -8,6 +9,7 @@
"port": 8117,
"password": "test"
},
"redisBlockNotifyListener": {
"redisPort": 6379,
"redisHost": "coindaemons.ultimatecoinpool.com",
@ -47,5 +49,9 @@
}
}
}
"website": {
"enabled": true,
"port": 80,
"liveStats": true
}
}

57
init.js
View File

@ -3,6 +3,7 @@ var os = require('os');
var cluster = require('cluster');
<<<<<<< HEAD
var posix = require('posix');
var PoolLogger = require('./libs/logutils.js');
var BlocknotifyListener = require('./libs/blocknotifyListener.js');
@ -14,15 +15,24 @@ var PaymentProcessor = require('./libs/paymentProcessor.js');
JSON.minify = JSON.minify || require("node-json-minify");
=======
var posix = require('posix');
var PoolLogger = require('./libs/logUtil.js');
var BlocknotifyListener = require('./libs/blocknotifyListener.js');
var WorkerListener = require('./libs/workerListener.js');
var PoolWorker = require('./libs/poolWorker.js');
var PaymentProcessor = require('./libs/paymentProcessor.js');
var Website = require('./libs/website.js');
JSON.minify = JSON.minify || require("node-json-minify");
var portalConfig = JSON.parse(JSON.minify(fs.readFileSync("config.json", {encoding: 'utf8'})));
>>>>>>> 0db53a296f9b77ad6ff76b5f06c7156d5366a777
var loggerInstance = new PoolLogger({
'default': true,
'keys': {
//'client' : 'warning',
'system' : true,
'submitblock' : true
}
logLevel: portalConfig.logLevel
});
var logDebug = loggerInstance.logDebug;
@ -49,6 +59,9 @@ if (cluster.isWorker){
case 'paymentProcessor':
new PaymentProcessor(loggerInstance);
break;
case 'website':
new Website(loggerInstance);
break;
}
return;
@ -112,7 +125,9 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){
});
worker.on('exit', function(code, signal){
logError('poolWorker', 'system', 'Fork ' + forkId + ' died, spawning replacement worker...');
createPoolWorker(forkId);
setTimeout(function(){
createPoolWorker(forkId);
}, 2000);
});
};
@ -170,13 +185,31 @@ var startPaymentProcessor = function(poolConfigs){
});
worker.on('exit', function(code, signal){
logError('paymentProcessor', 'system', 'Payment processor died, spawning replacement...');
startPaymentProcessor(poolConfigs);
setTimeout(function(){
startPaymentProcessor.apply(null, arguments);
}, 2000);
});
};
var startWebsite = function(portalConfig, poolConfigs){
if (!portalConfig.website.enabled) return;
var worker = cluster.fork({
workerType: 'website',
pools: JSON.stringify(poolConfigs),
portalConfig: JSON.stringify(portalConfig)
});
worker.on('exit', function(code, signal){
logError('website', 'system', 'Website process died, spawning replacement...');
setTimeout(function(){
startWebsite.apply(null, arguments);
}, 2000);
});
};
(function init(){
var portalConfig = JSON.parse(JSON.minify(fs.readFileSync("config.json", {encoding: 'utf8'})));
var poolConfigs = buildPoolConfigs();
@ -190,5 +223,11 @@ var startPaymentProcessor = function(poolConfigs){
startWorkerListener(poolConfigs);
<<<<<<< HEAD
})();
=======
startWebsite(portalConfig, poolConfigs);
})();
>>>>>>> 0db53a296f9b77ad6ff76b5f06c7156d5366a777

49
libs/api.js Normal file
View File

@ -0,0 +1,49 @@
var redis = require('redis');
var os = require('os');
module.exports = function(logger, poolConfigs){
var redisClients = [];
Object.keys(poolConfigs).forEach(function(coin){
var poolConfig = poolConfigs[coin];
var internalConfig = poolConfig.shareProcessing.internal;
var redisConfig = internalConfig.redis;
for (var i = 0; i < redisClients.length; i++){
var client = redisClients[i];
if (client.client.port === redisConfig.port && client.client.host === redisConfig.host){
client.coins.push(coin);
return;
}
}
redisClients.push({
coins: [coin],
client: redis.createClient(redisConfig.port, redisConfig.host)
});
});
//Every 10 minutes clear out old hashrate stats for each coin from redis
var clearExpiredHashrates = function(){
redisClients.forEach(function(client){
var tenMinutesAgo = (Date.now() / 1000 | 0) - (60 * 10);
var redisCommands = client.coins.map(function(coin){
return ['zremrangebyscore', coin + '_hashrate', '-inf', tenMinutesAgo];
});
client.client.multi(redisCommands).exec(function(err, replies){
if (err)
console.log('error with clearing old hashrates ' + JSON.stringify(err));
});
});
};
setInterval(clearExpiredHashrates, 10 * 60 * 1000);
clearExpiredHashrates();
this.getStats = function(callback){
//get stats like hashrate and in/valid shares/blocks and workers in current round
};
};

View File

@ -1,18 +0,0 @@
var express = require('express');
var os = require('os');
var app = express();
app.get('/getstatus', function (req, res) {
res.send({
'loadavg': os.loadavg(),
'freemem': os.freemem(),
});
});
module.exports = {
start: function () {
app.listen(9000);
}
}

View File

@ -39,35 +39,35 @@ var getSeverityColor = function(severity) {
var PoolLogger = function (configuration) {
var logLevelInt = severityToInt(configuration.logLevel);
// privates
var shouldLog = function(key, severity) {
var keyValue = configuration.keys[key];
if (typeof(keyValue) === 'undefined') {
keyValue = configuration.default;
}
if (typeof(keyValue) === 'boolean') {
return keyValue;
} else if (typeof(keyValue) === 'string') {
return severityToInt(severity) >= severityToInt(keyValue);
}
}
var severity = severityToInt(severity);
return severity >= logLevelInt;
};
var log = function(severity, key, poolName, text) {
if ( ! shouldLog(key, severity) ) {
// if this tag is set to not be logged or the default value is false then drop it!
//console.log(key+"DROPPED "+text + 'SEV' + severity);
if (!shouldLog(key, severity))
return;
}
var desc = poolName ? '[' + poolName + '] ' : '';
console.log(
<<<<<<< HEAD:libs/logutils.js
'\u001b['+getSeverityColor(severity)+'m' +
dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss') +
" ["+key+"]" + '\u001b[39m: ' + "\t" +
desc +
text);
}
=======
'\u001b[' + getSeverityColor(severity) + 'm' +
dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss') +
" [" + key + "]" + '\u001b[39m: ' + "\t" +
desc + text
);
};
>>>>>>> 0db53a296f9b77ad6ff76b5f06c7156d5366a777:libs/logUtil.js
// public

View File

@ -1,8 +1,11 @@
var redis = require('redis');
var async = require('async');
var Stratum = require('stratum-pool');
module.exports = function(logger){
var poolConfigs = JSON.parse(process.env.pools);
@ -76,43 +79,192 @@ function SetupForPool(logger, poolOptions){
connectToRedis();
var checkTx = function(tx, blockHeight){
daemon.cmd('gettransaction', [tx], function(results){
//console.dir(results[0].response.details[0].category);
var status = results[0].response.details[0].category;
var confirmed = (status === 'generate');
/* next:
- get contributed shares
- get unsent payments
- calculate payments
- send payments
- put unsent payments in db
- remove tx from db
- remove shares from db
var processPayments = function(){
async.waterfall([
/* Call redis to get an array of rounds - which are coinbase transactions and block heights from submitted
blocks. */
function(callback){
redisClient.smembers(coin + '_blocks', function(error, results){
if (error){
logger.error('redis', 'Could get blocks from redis ' + JSON.stringify(error));
callback('done - redis error for getting blocks');
return;
}
if (results.length === 0){
callback('done - no pending blocks in redis');
return;
}
var rounds = results.map(function(r){
var details = r.split(':');
return {txHash: details[0], height: details[1], reward: details[2]};
});
callback(null, rounds);
});
},
/* Does a batch rpc call to daemon with all the transaction hashes to see if they are confirmed yet.
It also adds the block reward amount to the round object - which the daemon gives also gives us. */
function(rounds, callback){
var batchRPCcommand = rounds.map(function(r){
return ['gettransaction', [r.txHash]];
});
daemon.batchCmd(batchRPCcommand, function(error, txDetails){
if (error || !txDetails){
callback('done - daemon rpc error with batch gettransactions ' + JSON.stringify(error));
return;
}
//Rounds that are not confirmed yet are removed from the round array
//We also get reward amount for each block from daemon reply
rounds = rounds.filter(function(r){
var tx = txDetails.filter(function(t){ return t.result.txid === r.txHash; })[0];
if (tx.result.details[0].category !== 'generate') return false;
r.amount = tx.result.amount;
r.magnitude = r.reward / r.amount;
return true;
});
if (rounds.length === 0){
callback('done - no confirmed transactions yet');
return;
}
callback(null, rounds);
});
},
/* Does a batch redis call to get shares contributed to each round. Then calculates the reward
amount owned to each miner for each round. */
function(rounds, callback){
var shareLookups = rounds.map(function(r){
return ['hgetall', coin + '_shares:round' + r.height]
});
redisClient.multi(shareLookups).exec(function(error, allWorkerShares){
if (error){
callback('done - redis error with multi get rounds share')
return;
}
var workerRewards = {};
for (var i = 0; i < rounds.length; i++){
var round = rounds[i];
var workerShares = allWorkerShares[i];
var reward = round.reward * (1 - processingConfig.feePercent);
var totalShares = Object.keys(workerShares).reduce(function(p, c){
return p + parseInt(workerShares[c])
}, 0);
for (var worker in workerShares){
var percent = parseInt(workerShares[worker]) / totalShares;
var workerRewardTotal = Math.floor(reward * percent);
if (!(worker in workerRewards)) workerRewards[worker] = 0;
workerRewards[worker] += workerRewardTotal;
}
}
//this calculates profit if you wanna see it
/*
var workerTotalRewards = Object.keys(workerRewards).reduce(function(p, c){
return p + workerRewards[c];
}, 0);
var poolTotalRewards = rounds.reduce(function(p, c){
return p + c.amount * c.magnitude;
}, 0);
console.log(workerRewards);
console.log('pool profit percent' + ((poolTotalRewards - workerTotalRewards) / poolTotalRewards));
*/
callback(null, rounds, workerRewards);
});
},
/* Does a batch call to redis to get worker existing balances from coin_balances*/
function(rounds, workerRewards, callback){
var workers = Object.keys(workerRewards);
redisClient.hmget([coin + '_balances'].concat(workers), function(error, results){
if (error){
callback('done - redis error with multi get rounds share')
return;
}
var workerBalances = {};
for (var i = 0; i < workers.length; i++){
workerBalances[workers[i]] = parseInt(results[i]) || 0;
}
callback(null, rounds, workerRewards, workerBalances)
});
},
/* Calculate if any payments are ready to be sent and trigger them sending
Get balance different for each address and pass it along as object of latest balances such as
{worker1: balance1, worker2, balance2}
when deciding the sent balance, it the difference should be -1*amount they had in db,
if not sending the balance, the differnce should be +(the amount they earned this round)
*/
function(rounds, workerRewards, workerBalances, callback){
/* if payments dont succeed (likely because daemon isnt responding to rpc), then cancel here
so that all of this can be tried again when the daemon is working. otherwise we will consider
payment sent after we cleaned up the db.
*/
/* In here do daemon.getbalance, figure out how many payments should be sent, see if the
remaining balance after payments-to-be sent is greater than the min reserver, otherwise
put everything in worker balances to be paid next time.
*/
},
/* clean DB: update remaining balances in coin_balance hashset in redis
*/
function(balanceDifference, rounds, callback){
//SMOVE each tx key from coin_blocks to coin_processedBlocks
//HINCRBY to apply balance different for coin_balances worker1
}
], function(error, result){
console.log(error);
//log error completion
});
};
setInterval(function(){
redisClient.smembers('blocks_' + coin, function(error, results){
if (error){
logger.error('redis', 'Could get blocks from redis ' + JSON.stringify(error));
return;
}
results.forEach(function(item){
var split = item.split(':');
var tx = split[0];
var blockHeight = split[1];
checkTx(tx, blockHeight);
});
});
}, processingConfig.paymentInterval * 1000);
setInterval(processPayments, processingConfig.paymentInterval * 1000);
setTimeout(processPayments, 100);
};

View File

@ -19,9 +19,6 @@ module.exports = function(logger, poolConfig){
var redisConfig = internalConfig.redis;
var coin = poolConfig.coin.name;
var connection;
function connect(){
@ -47,22 +44,42 @@ module.exports = function(logger, poolConfig){
connect();
this.handleShare = function(isValidShare, isValidBlock, shareData){
if (!isValidShare) return;
var redisCommands = [];
connection.hincrby(['shares_' + coin + ':' + shareData.height, shareData.worker, shareData.difficulty], function(error, result){
if (error)
logger.error('redis', 'Could not store worker share')
});
if (isValidShare){
redisCommands.push(['hincrby', coin + '_shares:roundCurrent', shareData.worker, shareData.difficulty]);
redisCommands.push(['hincrby', coin + '_stats', 'validShares', 1]);
/* Stores share diff, worker, and unique value with a score that is the timestamp. Unique value ensures it
doesn't overwrite an existing entry, and timestamp as score lets us query shares from last X minutes to
generate hashrate for each worker and pool. */
redisCommands.push(['zadd', coin + '_hashrate', Date.now() / 1000 | 0, [shareData.difficulty, shareData.worker, Math.random()].join(':')]);
}
else{
redisCommands.push(['hincrby', coin + '_stats', 'invalidShares', 1]);
}
if (isValidBlock){
connection.sadd(['blocks_' + coin, shareData.tx + ':' + shareData.height], function(error, result){
if (error)
logger.error('redis', 'Could not store block data');
});
redisCommands.push(['rename', coin + '_shares:roundCurrent', coin + '_shares:round' + shareData.height]);
redisCommands.push(['sadd', coin + '_blocks', shareData.tx + ':' + shareData.height + ':' + shareData.reward]);
redisCommands.push(['hincrby', coin + '_stats', 'validBlocks', 1]);
}
else if (shareData.solution){
redisCommands.push(['hincrby', coin + '_stats', 'invalidBlocks', 1]);
}
connection.multi(redisCommands).exec(function(err, replies){
if (err)
console.log('error with share processor multi ' + JSON.stringify(err));
else{
console.log(JSON.stringify(replies));
}
});
};

View File

@ -1,12 +1,155 @@
/* TODO
listen on port 80 for requests, maybe use express.
read website folder files into memory, and use fs.watch to reload changes to any files into memory
on some interval, apply a templating process to it with the latest api stats. on http requests, serve
this templated file and the other resources in memory.
Need to condense the entire website into a single html page. Embedding the javascript and css is easy. For images,
hopefully we can only use svg which can be embedded - otherwise we can convert the image into a data-url that can
be embedded, Favicon can also be a data-url which some javascript kungfu can display in browser. I'm focusing on
this mainly to help mitigate ddos and other kinds of attacks - and to just have a badass blazing fast project.
ideally, all css/js should be included in the html file (try to avoid images, uses embeddable svg)
this would give us one file to have to serve
Don't worry about doing any of that condensing yourself - go head and keep all the resources as separate files.
I will write a script for when the server starts to read all the files in the /website folder and minify and condense
it all together into one file, saved in memory. We will have 1 persistent condensed file that servers as our "template"
file that contains things like:
<div>Hashrate: {{=stats.hashrate}</div>
*/
And then on some caching interval (maybe 5 seconds?) we will apply the template engine to generate the real html page
that we serve and hold in in memory - this is the file we serve to seo-bots (googlebot) and users when they first load
the page.
Once the user loads the page we will have server-side event source connected to the portal api where it receives
updated stats on some interval (probably 5 seconds like template cache updater) and applies the changes to the already
displayed page.
We will use fs.watch to detect changes to anything in the /website folder and update our stuff in memory.
*/
var fs = require('fs');
var path = require('path');
var async = require('async');
var dot = require('dot');
var express = require('express');
var api = require('./api.js');
module.exports = function(logger){
var portalConfig = JSON.parse(process.env.portalConfig);
var poolConfigs = JSON.parse(process.env.pools);
var portalApi = new api(logger, poolConfigs);
var logIdentify = 'Website';
var websiteLogger = {
debug: function(key, text){
logger.logDebug(logIdentify, key, text);
},
warning: function(key, text){
logger.logWarning(logIdentify, key, text);
},
error: function(key, text){
logger.logError(logIdentify, key, text);
}
};
var pageResources = '';
var pageTemplate;
var pageProcessed = '';
var loadWebPage = function(callback){
fs.readdir('website', function(err, files){
async.map(files, function(fileName, callback){
var filePath = 'website/' + fileName;
fs.readFile(filePath, 'utf8', function(err, data){
callback(null, {name: fileName, data: data, ext: path.extname(filePath)});
});
}, function(err, fileObjects){
var indexPage = fileObjects.filter(function(f){
return f.name === 'index.html';
})[0].data;
var jsCode = '<script>';
var cssCode = '<style>';
fileObjects.forEach(function(f){
switch(f.ext){
case '.css':
cssCode += (f.data + '\n\n\n\n');
break;
case '.js':
jsCode += (f.data + ';\n\n\n\n');
break;
}
});
jsCode += '</script>';
cssCode += '</style>';
var bodyIndex = indexPage.indexOf('<body>');
pageTemplate = dot.template(indexPage.slice(bodyIndex));
pageResources = indexPage.slice(0, bodyIndex);
var headIndex = pageResources.indexOf('</head>');
pageResources = pageResources.slice(0, headIndex) +
jsCode + '\n\n\n\n' +
cssCode + '\n\n\n\n' +
pageResources.slice(headIndex);
applyTemplateInfo();
callback || function(){}();
})
});
};
loadWebPage();
var applyTemplateInfo = function(){
portalApi.getStats(function(stats){
//need to give template info about pools and stats
pageProcessed = pageTemplate({test: 'visitor', time: Date.now()});
});
};
setInterval(function(){
applyTemplateInfo();
}, 5000);
var reloadTimeout;
fs.watch('website', function(){
clearTimeout(reloadTimeout);
reloadTimeout = setTimeout(function(){
loadWebPage();
}, 500);
});
var app = express();
//need to create a stats api endpoint for eventsource live stat updates which are triggered on the applytemplateinfo interval
app.get('/', function(req, res){
res.send(pageResources + pageProcessed);
});
app.use(function(err, req, res, next){
console.error(err.stack);
res.send(500, 'Something broke!');
});
app.listen(portalConfig.website.port, function(){
websiteLogger.debug('system', 'Website started on port ' + portalConfig.website.port);
});
};

View File

@ -36,7 +36,10 @@
"node-json-minify": "*",
"posix": "*",
"redis": "*",
"mysql": "*"
"mysql": "*",
"async": "*",
"express": "*",
"dot": "*"
},
"engines": {
"node": ">=0.10"

View File

@ -8,9 +8,9 @@
"validateWorkerAddress": true,
"paymentInterval": 10,
"minimumPayment": 0.001,
"minimumReserve": 10,
"feePercent": 0.02,
"feeReceiveAddress": "LZz44iyF4zLCXJTU8RxztyyJZBntdS6fvv",
"minimumReserve": 10,
"feeWithdrawalThreshold": 5,
"daemon": {
"host": "localhost",

39
website/Chart.min.js vendored Normal file
View File

@ -0,0 +1,39 @@
var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a=
Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);d<b||d>c;)a=d<b?a/2:2*a,d=Math.round(e/a);c=[];z(f,c,d,h,a);return{steps:d,stepValue:a,graphMin:h,labels:c}}function z(a,c,b,e,h){if(a)for(var f=1;f<b+1;f++)c.push(E(a,{value:(e+h*f).toFixed(0!=h%1?h.toString().split(".")[1].length:0)}))}function A(a,c,b){return!isNaN(parseFloat(c))&&isFinite(c)&&a>c?c:!isNaN(parseFloat(b))&&
isFinite(b)&&a<b?b:a}function y(a,c){var b={},e;for(e in a)b[e]=a[e];for(e in c)b[e]=c[e];return b}function E(a,c){var b=!/\W/.test(a)?F[a]=F[a]||E(document.getElementById(a).innerHTML):new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+a.replace(/[\r\t\n]/g," ").split("<%").join("\t").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c?
b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)?
0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1==
a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*
Math.PI)*Math.asin(1/e);return-(e*Math.pow(2,10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b))},easeOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return e*Math.pow(2,-10*a)*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(2==(a/=0.5))return 1;b||(b=1*0.3*1.5);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return 1>a?-0.5*e*Math.pow(2,10*
(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)*
a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0,
scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",
animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",
scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a,
c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,
onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0,
pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",
scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]);
d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.length;f++)a[f].value>e&&(e=a[f].value),a[f].value<h&&(h=a[f].value);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,
m);k=g/j.steps;x(c,function(){for(var a=0;a<j.steps;a++)if(c.scaleShowLine&&(b.beginPath(),b.arc(q/2,u/2,k*(a+1),0,2*Math.PI,!0),b.strokeStyle=c.scaleLineColor,b.lineWidth=c.scaleLineWidth,b.stroke()),c.scaleShowLabels){b.textAlign="center";b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;var e=j.labels[a];if(c.scaleShowLabelBackdrop){var d=b.measureText(e).width;b.fillStyle=c.scaleBackdropColor;b.beginPath();b.rect(Math.round(q/2-d/2-c.scaleBackdropPaddingX),Math.round(u/2-k*(a+
1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(d+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY));b.fill()}b.textBaseline="middle";b.fillStyle=c.scaleFontColor;b.fillText(e,q/2,u/2-k*(a+1))}},function(e){var d=-Math.PI/2,g=2*Math.PI/a.length,f=1,h=1;c.animation&&(c.animateScale&&(f=e),c.animateRotate&&(h=e));for(e=0;e<a.length;e++)b.beginPath(),b.arc(q/2,u/2,f*v(a[e].value,j,k),d,d+h*g,!1),b.lineTo(q/2,u/2),b.closePath(),b.fillStyle=a[e].color,b.fill(),
c.segmentShowStroke&&(b.strokeStyle=c.segmentStrokeColor,b.lineWidth=c.segmentStrokeWidth,b.stroke()),d+=h*g},b)},H=function(a,c,b){var e,h,f,d,g,k,j,l,m;a.labels||(a.labels=[]);g=Math.min.apply(Math,[q,u])/2;d=2*c.scaleFontSize;for(e=l=0;e<a.labels.length;e++)b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily,h=b.measureText(a.labels[e]).width,h>l&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE;
h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(m=0;m<a.datasets[f].data.length;m++)a.datasets[f].data[m]>e&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]<h&&(h=a.datasets[f].data[m]);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,m);k=g/j.steps;x(c,function(){var e=2*Math.PI/
a.datasets[0].data.length;b.save();b.translate(q/2,u/2);if(c.angleShowLineOut){b.strokeStyle=c.angleLineColor;b.lineWidth=c.angleLineWidth;for(var d=0;d<a.datasets[0].data.length;d++)b.rotate(e),b.beginPath(),b.moveTo(0,0),b.lineTo(0,-g),b.stroke()}for(d=0;d<j.steps;d++){b.beginPath();if(c.scaleShowLine){b.strokeStyle=c.scaleLineColor;b.lineWidth=c.scaleLineWidth;b.moveTo(0,-k*(d+1));for(var f=0;f<a.datasets[0].data.length;f++)b.rotate(e),b.lineTo(0,-k*(d+1));b.closePath();b.stroke()}c.scaleShowLabels&&
(b.textAlign="center",b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily,b.textBaseline="middle",c.scaleShowLabelBackdrop&&(f=b.measureText(j.labels[d]).width,b.fillStyle=c.scaleBackdropColor,b.beginPath(),b.rect(Math.round(-f/2-c.scaleBackdropPaddingX),Math.round(-k*(d+1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(f+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY)),b.fill()),b.fillStyle=c.scaleFontColor,b.fillText(j.labels[d],0,-k*(d+
1)))}for(d=0;d<a.labels.length;d++){b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily;b.fillStyle=c.pointLabelFontColor;var f=Math.sin(e*d)*(g+c.pointLabelFontSize),h=Math.cos(e*d)*(g+c.pointLabelFontSize);b.textAlign=e*d==Math.PI||0==e*d?"center":e*d>Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;g<a.datasets.length;g++){b.beginPath();
b.moveTo(0,d*-1*v(a.datasets[g].data[0],j,k));for(var f=1;f<a.datasets[g].data.length;f++)b.rotate(e),b.lineTo(0,d*-1*v(a.datasets[g].data[f],j,k));b.closePath();b.fillStyle=a.datasets[g].fillColor;b.strokeStyle=a.datasets[g].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.fill();b.stroke();if(c.pointDot){b.fillStyle=a.datasets[g].pointColor;b.strokeStyle=a.datasets[g].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(f=0;f<a.datasets[g].data.length;f++)b.rotate(e),b.beginPath(),b.arc(0,d*-1*
v(a.datasets[g].data[f],j,k),c.pointDotRadius,2*Math.PI,!1),b.fill(),b.stroke()}b.rotate(e)}b.restore()},b)},I=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=0;f<a.length;f++)e+=a[f].value;x(c,null,function(d){var g=-Math.PI/2,f=1,j=1;c.animation&&(c.animateScale&&(f=d),c.animateRotate&&(j=d));for(d=0;d<a.length;d++){var l=j*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,f*h,g,g+l);b.lineTo(q/2,u/2);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&(b.lineWidth=
c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());g+=l}},b)},J=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=h*(c.percentageInnerCutout/100),d=0;d<a.length;d++)e+=a[d].value;x(c,null,function(d){var k=-Math.PI/2,j=1,l=1;c.animation&&(c.animateScale&&(j=d),c.animateRotate&&(l=d));for(d=0;d<a.length;d++){var m=l*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,j*h,k,k+m,!1);b.arc(q/2,u/2,j*f,k+m,k,!0);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&
(b.lineWidth=c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());k+=m}},b)},K=function(a,c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(s=45,q/a.labels.length<Math.cos(s)*t?(s=90,g-=t):g-=Math.sin(s)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=
0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]<h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;
for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<s?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<s?(b.translate(n+d*m,p+c.scaleFontSize),b.rotate(-(s*(Math.PI/180))),b.fillText(a.labels[d],
0,0),b.restore()):b.fillText(a.labels[d],n+d*m,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+d*m,p+3),c.scaleShowGridLines&&0<d?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+d*m,5)):b.lineTo(n+d*m,p+3),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,
b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){function e(b,c){return p-d*v(a.datasets[b].data[c],j,k)}for(var f=0;f<a.datasets.length;f++){b.strokeStyle=a.datasets[f].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.beginPath();b.moveTo(n,p-d*v(a.datasets[f].data[0],j,k));for(var g=1;g<a.datasets[f].data.length;g++)c.bezierCurve?b.bezierCurveTo(n+m*(g-0.5),e(f,g-1),n+m*(g-0.5),
e(f,g),n+m*g,e(f,g)):b.lineTo(n+m*g,e(f,g));b.stroke();c.datasetFill?(b.lineTo(n+m*(a.datasets[f].data.length-1),p),b.lineTo(n,p),b.closePath(),b.fillStyle=a.datasets[f].fillColor,b.fill()):b.closePath();if(c.pointDot){b.fillStyle=a.datasets[f].pointColor;b.strokeStyle=a.datasets[f].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(g=0;g<a.datasets[f].data.length;g++)b.beginPath(),b.arc(n+m*g,p-d*v(a.datasets[f].data[g],j,k),c.pointDotRadius,0,2*Math.PI,!0),b.fill(),b.stroke()}}},b)},L=function(a,
c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s,w=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(w=45,q/a.labels.length<Math.cos(w)*t?(w=90,g-=t):g-=Math.sin(w)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]<
h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m=
Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<w?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<w?(b.translate(n+
d*m,p+c.scaleFontSize),b.rotate(-(w*(Math.PI/180))),b.fillText(a.labels[d],0,0),b.restore()):b.fillText(a.labels[d],n+d*m+m/2,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+(d+1)*m,p+3),b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+(d+1)*m,5),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*
k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){b.lineWidth=c.barStrokeWidth;for(var e=0;e<a.datasets.length;e++){b.fillStyle=a.datasets[e].fillColor;b.strokeStyle=a.datasets[e].strokeColor;for(var f=0;f<a.datasets[e].data.length;f++){var g=n+c.barValueSpacing+m*f+s*e+c.barDatasetSpacing*e+c.barStrokeWidth*e;b.beginPath();
b.moveTo(g,p);b.lineTo(g,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p);c.barShowStroke&&b.stroke();b.closePath();b.fill()}}},b)},D=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(a,1E3/60)},F={}};

View File

@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Title</title>
</head>
<body>
<div>Hello {{=it.test}}!</div>
<div>The unix time at page generation is {{=it.time}}</div>
<div>Need to build a pretty page here...</div>
</body>
</html>

0
website/main.js Normal file
View File

11
website/pure-min.css vendored Normal file

File diff suppressed because one or more lines are too long

0
website/style.css Normal file
View File

2
website/zepto.min.js vendored Normal file

File diff suppressed because one or more lines are too long