diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a50d4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.idea/ +config.json +pool_configs/*.json +!pool_configs/zclassic_example.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3b4562c --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Old/CHANGELOG.md b/Old/CHANGELOG.md new file mode 100644 index 0000000..eb32ffb --- /dev/null +++ b/Old/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +### 20161127 + * Fixed payment processor crashes + * Fixed varDiff and set default to 0.125 + * Fixed merkleRoot issue with multiple transactions in each block + * Fixed incorrect hashrates + * Begin compatibility testing with Node 7+ + * Added t_address -> z_address -> t_address coin transfer function + + diff --git a/coins/komodo.json b/coins/komodo.json new file mode 100644 index 0000000..cd3b1bd --- /dev/null +++ b/coins/komodo.json @@ -0,0 +1,7 @@ +{ + "name": "komodo", + "symbol": "KMD", + "algorithm": "equihash", + "peerMagic": "f9eee48d", + "txfee": 0.0001 +} \ No newline at end of file diff --git a/init.js b/init.js new file mode 100644 index 0000000..1abe554 --- /dev/null +++ b/init.js @@ -0,0 +1,517 @@ +var fs = require('fs'); +var path = require('path'); +var os = require('os'); +var cluster = require('cluster'); + +var async = require('async'); +var extend = require('extend'); + +var redis = require('redis'); + +var PoolLogger = require('./libs/logUtil.js'); +var CliListener = require('./libs/cliListener.js'); +var PoolWorker = require('./libs/poolWorker.js'); +var PaymentProcessor = require('./libs/paymentProcessor.js'); +var Website = require('./libs/website.js'); +var ProfitSwitch = require('./libs/profitSwitch.js'); + +var algos = require('stratum-pool/lib/algoProperties.js'); + +JSON.minify = JSON.minify || require("node-json-minify"); + +if (!fs.existsSync('config.json')){ + console.log('config.json file does not exist. Read the installation/setup instructions.'); + return; +} + +var portalConfig = JSON.parse(JSON.minify(fs.readFileSync("config.json", {encoding: 'utf8'}))); +var poolConfigs; + + +var logger = new PoolLogger({ + logLevel: portalConfig.logLevel, + logColors: portalConfig.logColors +}); + +try { + require('newrelic'); + if (cluster.isMaster) + logger.debug('NewRelic', 'Monitor', 'New Relic initiated'); +} catch(e) {} + + +//Try to give process ability to handle 100k concurrent connections +try{ + var posix = require('posix'); + try { + posix.setrlimit('nofile', { soft: 100000, hard: 100000 }); + } + catch(e){ + if (cluster.isMaster) + logger.warning('POSIX', 'Connection Limit', '(Safe to ignore) Must be ran as root to increase resource limits'); + } + finally { + // Find out which user used sudo through the environment variable + var uid = parseInt(process.env.SUDO_UID); + // Set our server's uid to that user + if (uid) { + process.setuid(uid); + logger.debug('POSIX', 'Connection Limit', 'Raised to 100K concurrent connections, now running as non-root user: ' + process.getuid()); + } + } +} +catch(e){ + if (cluster.isMaster) + logger.debug('POSIX', 'Connection Limit', '(Safe to ignore) POSIX module not installed and resource (connection) limit was not raised'); +} + +if (cluster.isWorker){ + + switch(process.env.workerType){ + case 'pool': + new PoolWorker(logger); + break; + case 'paymentProcessor': + new PaymentProcessor(logger); + break; + case 'website': + new Website(logger); + break; + case 'profitSwitch': + new ProfitSwitch(logger); + break; + } + + return; +} + + +//Read all pool configs from pool_configs and join them with their coin profile +var buildPoolConfigs = function(){ + var configs = {}; + var configDir = 'pool_configs/'; + + var poolConfigFiles = []; + + + /* Get filenames of pool config json files that are enabled */ + fs.readdirSync(configDir).forEach(function(file){ + if (!fs.existsSync(configDir + file) || path.extname(configDir + file) !== '.json') return; + var poolOptions = JSON.parse(JSON.minify(fs.readFileSync(configDir + file, {encoding: 'utf8'}))); + if (!poolOptions.enabled) return; + poolOptions.fileName = file; + poolConfigFiles.push(poolOptions); + }); + + + /* Ensure no pool uses any of the same ports as another pool */ + for (var i = 0; i < poolConfigFiles.length; i++){ + var ports = Object.keys(poolConfigFiles[i].ports); + for (var f = 0; f < poolConfigFiles.length; f++){ + if (f === i) continue; + var portsF = Object.keys(poolConfigFiles[f].ports); + for (var g = 0; g < portsF.length; g++){ + if (ports.indexOf(portsF[g]) !== -1){ + logger.error('Master', poolConfigFiles[f].fileName, 'Has same configured port of ' + portsF[g] + ' as ' + poolConfigFiles[i].fileName); + process.exit(1); + return; + } + } + + if (poolConfigFiles[f].coin === poolConfigFiles[i].coin){ + logger.error('Master', poolConfigFiles[f].fileName, 'Pool has same configured coin file coins/' + poolConfigFiles[f].coin + ' as ' + poolConfigFiles[i].fileName + ' pool'); + process.exit(1); + return; + } + + } + } + + + poolConfigFiles.forEach(function(poolOptions){ + + poolOptions.coinFileName = poolOptions.coin; + + var coinFilePath = 'coins/' + poolOptions.coinFileName; + if (!fs.existsSync(coinFilePath)){ + logger.error('Master', poolOptions.coinFileName, 'could not find file: ' + coinFilePath); + return; + } + + var coinProfile = JSON.parse(JSON.minify(fs.readFileSync(coinFilePath, {encoding: 'utf8'}))); + poolOptions.coin = coinProfile; + poolOptions.coin.name = poolOptions.coin.name.toLowerCase(); + + if (poolOptions.coin.name in configs){ + + logger.error('Master', poolOptions.fileName, 'coins/' + poolOptions.coinFileName + + ' has same configured coin name ' + poolOptions.coin.name + ' as coins/' + + configs[poolOptions.coin.name].coinFileName + ' used by pool config ' + + configs[poolOptions.coin.name].fileName); + + process.exit(1); + return; + } + + for (var option in portalConfig.defaultPoolConfigs){ + if (!(option in poolOptions)){ + var toCloneOption = portalConfig.defaultPoolConfigs[option]; + var clonedOption = {}; + if (toCloneOption.constructor === Object) + extend(true, clonedOption, toCloneOption); + else + clonedOption = toCloneOption; + poolOptions[option] = clonedOption; + } + } + + + configs[poolOptions.coin.name] = poolOptions; + + if (!(coinProfile.algorithm in algos)){ + logger.error('Master', coinProfile.name, 'Cannot run a pool for unsupported algorithm "' + coinProfile.algorithm + '"'); + delete configs[poolOptions.coin.name]; + } + + }); + return configs; +}; + +function roundTo(n, digits) { + if (digits === undefined) { + digits = 0; + } + var multiplicator = Math.pow(10, digits); + n = parseFloat((n * multiplicator).toFixed(11)); + var test =(Math.round(n) / multiplicator); + return +(test.toFixed(digits)); +} + +var _lastStartTimes = []; +var _lastShareTimes = []; + +var spawnPoolWorkers = function(){ + + var redisConfig; + var connection; + + Object.keys(poolConfigs).forEach(function(coin){ + var pcfg = poolConfigs[coin]; + if (!Array.isArray(pcfg.daemons) || pcfg.daemons.length < 1){ + logger.error('Master', coin, 'No daemons configured so a pool cannot be started for this coin.'); + delete poolConfigs[coin]; + } else if (!connection) { + redisConfig = pcfg.redis; + connection = redis.createClient(redisConfig.port, redisConfig.host); + connection.on('ready', function(){ + logger.debug('PPLNT', coin, 'TimeShare processing setup with redis (' + redisConfig.host + + ':' + redisConfig.port + ')'); + }); + } + }); + + if (Object.keys(poolConfigs).length === 0){ + logger.warning('Master', 'PoolSpawner', 'No pool configs exists or are enabled in pool_configs folder. No pools spawned.'); + return; + } + + + var serializedConfigs = JSON.stringify(poolConfigs); + + var numForks = (function(){ + if (!portalConfig.clustering || !portalConfig.clustering.enabled) + return 1; + if (portalConfig.clustering.forks === 'auto') + return os.cpus().length; + if (!portalConfig.clustering.forks || isNaN(portalConfig.clustering.forks)) + return 1; + return portalConfig.clustering.forks; + })(); + + var poolWorkers = {}; + + var createPoolWorker = function(forkId){ + var worker = cluster.fork({ + workerType: 'pool', + forkId: forkId, + pools: serializedConfigs, + portalConfig: JSON.stringify(portalConfig) + }); + worker.forkId = forkId; + worker.type = 'pool'; + poolWorkers[forkId] = worker; + worker.on('exit', function(code, signal){ + logger.error('Master', 'PoolSpawner', 'Fork ' + forkId + ' died, spawning replacement worker...'); + setTimeout(function(){ + createPoolWorker(forkId); + }, 2000); + }).on('message', function(msg){ + switch(msg.type){ + case 'banIP': + Object.keys(cluster.workers).forEach(function(id) { + if (cluster.workers[id].type === 'pool'){ + cluster.workers[id].send({type: 'banIP', ip: msg.ip}); + } + }); + break; + case 'shareTrack': + // pplnt time share tracking of workers + if (msg.isValidShare && !msg.isValidBlock) { + var now = Date.now(); + var lastShareTime = now; + var lastStartTime = now; + var workerAddress = msg.data.worker.split('.')[0]; + + // if needed, initialize PPLNT objects for coin + if (!_lastShareTimes[msg.coin]) { + _lastShareTimes[msg.coin] = {}; + } + if (!_lastStartTimes[msg.coin]) { + _lastStartTimes[msg.coin] = {}; + } + + // did they just join in this round? + if (!_lastShareTimes[msg.coin][workerAddress] || !_lastStartTimes[msg.coin][workerAddress]) { + _lastShareTimes[msg.coin][workerAddress] = now; + _lastStartTimes[msg.coin][workerAddress] = now; + logger.debug('PPLNT', msg.coin, 'Thread '+msg.thread, workerAddress+' joined.'); + } + // grab last times from memory objects + if (_lastShareTimes[msg.coin][workerAddress] != null && _lastShareTimes[msg.coin][workerAddress] > 0) { + lastShareTime = _lastShareTimes[msg.coin][workerAddress]; + lastStartTime = _lastStartTimes[msg.coin][workerAddress]; + } + + var redisCommands = []; + + // if its been less than 15 minutes since last share was submitted + var timeChangeSec = roundTo(Math.max(now - lastShareTime, 0) / 1000, 4); + //var timeChangeTotal = roundTo(Math.max(now - lastStartTime, 0) / 1000, 4); + if (timeChangeSec < 900) { + // loyal miner keeps mining :) + redisCommands.push(['hincrbyfloat', msg.coin + ':shares:timesCurrent', workerAddress + "." + poolConfigs[msg.coin].poolId, timeChangeSec]); + //logger.debug('PPLNT', msg.coin, 'Thread '+msg.thread, workerAddress+':{totalTimeSec:'+timeChangeTotal+', timeChangeSec:'+timeChangeSec+'}'); + connection.multi(redisCommands).exec(function(err, replies){ + if (err) + logger.error('PPLNT', msg.coin, 'Thread '+msg.thread, 'Error with time share processor call to redis ' + JSON.stringify(err)); + }); + } else { + // they just re-joined the pool + _lastStartTimes[workerAddress] = now; + logger.debug('PPLNT', msg.coin, 'Thread '+msg.thread, workerAddress+' re-joined.'); + } + + // track last time share + _lastShareTimes[msg.coin][workerAddress] = now; + } + if (msg.isValidBlock) { + // reset pplnt share times for next round + _lastShareTimes[msg.coin] = {}; + _lastStartTimes[msg.coin] = {}; + } + break; + } + }); + }; + + var i = 0; + var spawnInterval = setInterval(function(){ + createPoolWorker(i); + i++; + if (i === numForks){ + clearInterval(spawnInterval); + logger.debug('Master', 'PoolSpawner', 'Spawned ' + Object.keys(poolConfigs).length + ' pool(s) on ' + numForks + ' thread(s)'); + } + }, 250); + +}; + + +var startCliListener = function(){ + + var cliPort = portalConfig.cliPort; + + var listener = new CliListener(cliPort); + listener.on('log', function(text){ + logger.debug('Master', 'CLI', text); + }).on('command', function(command, params, options, reply){ + + switch(command){ + case 'blocknotify': + Object.keys(cluster.workers).forEach(function(id) { + cluster.workers[id].send({type: 'blocknotify', coin: params[0], hash: params[1]}); + }); + reply('Pool workers notified'); + break; + case 'coinswitch': + processCoinSwitchCommand(params, options, reply); + break; + case 'reloadpool': + Object.keys(cluster.workers).forEach(function(id) { + cluster.workers[id].send({type: 'reloadpool', coin: params[0] }); + }); + reply('reloaded pool ' + params[0]); + break; + default: + reply('unrecognized command "' + command + '"'); + break; + } + }).start(); +}; + + +var processCoinSwitchCommand = function(params, options, reply){ + + var logSystem = 'CLI'; + var logComponent = 'coinswitch'; + + var replyError = function(msg){ + reply(msg); + logger.error(logSystem, logComponent, msg); + }; + + if (!params[0]) { + replyError('Coin name required'); + return; + } + + if (!params[1] && !options.algorithm){ + replyError('If switch key is not provided then algorithm options must be specified'); + return; + } + else if (params[1] && !portalConfig.switching[params[1]]){ + replyError('Switch key not recognized: ' + params[1]); + return; + } + else if (options.algorithm && !Object.keys(portalConfig.switching).filter(function(s){ + return portalConfig.switching[s].algorithm === options.algorithm; + })[0]){ + replyError('No switching options contain the algorithm ' + options.algorithm); + return; + } + + var messageCoin = params[0].toLowerCase(); + var newCoin = Object.keys(poolConfigs).filter(function(p){ + return p.toLowerCase() === messageCoin; + })[0]; + + if (!newCoin){ + replyError('Switch message to coin that is not recognized: ' + messageCoin); + return; + } + + + var switchNames = []; + + if (params[1]) { + switchNames.push(params[1]); + } + else{ + for (var name in portalConfig.switching){ + if (portalConfig.switching[name].enabled && portalConfig.switching[name].algorithm === options.algorithm) + switchNames.push(name); + } + } + + switchNames.forEach(function(name){ + if (poolConfigs[newCoin].coin.algorithm !== portalConfig.switching[name].algorithm){ + replyError('Cannot switch a ' + + portalConfig.switching[name].algorithm + + ' algo pool to coin ' + newCoin + ' with ' + poolConfigs[newCoin].coin.algorithm + ' algo'); + return; + } + + Object.keys(cluster.workers).forEach(function (id) { + cluster.workers[id].send({type: 'coinswitch', coin: newCoin, switchName: name }); + }); + }); + + reply('Switch message sent to pool workers'); + +}; + + + +var startPaymentProcessor = function(){ + + var enabledForAny = false; + for (var pool in poolConfigs){ + var p = poolConfigs[pool]; + var enabled = p.enabled && p.paymentProcessing && p.paymentProcessing.enabled; + if (enabled){ + enabledForAny = true; + break; + } + } + + if (!enabledForAny) + return; + + var worker = cluster.fork({ + workerType: 'paymentProcessor', + pools: JSON.stringify(poolConfigs) + }); + worker.on('exit', function(code, signal){ + logger.error('Master', 'Payment Processor', 'Payment processor died, spawning replacement...'); + setTimeout(function(){ + startPaymentProcessor(poolConfigs); + }, 2000); + }); +}; + + +var startWebsite = function(){ + + 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){ + logger.error('Master', 'Website', 'Website process died, spawning replacement...'); + setTimeout(function(){ + startWebsite(portalConfig, poolConfigs); + }, 2000); + }); +}; + + +var startProfitSwitch = function(){ + + if (!portalConfig.profitSwitch || !portalConfig.profitSwitch.enabled){ + //logger.error('Master', 'Profit', 'Profit auto switching disabled'); + return; + } + + var worker = cluster.fork({ + workerType: 'profitSwitch', + pools: JSON.stringify(poolConfigs), + portalConfig: JSON.stringify(portalConfig) + }); + worker.on('exit', function(code, signal){ + logger.error('Master', 'Profit', 'Profit switching process died, spawning replacement...'); + setTimeout(function(){ + startWebsite(portalConfig, poolConfigs); + }, 2000); + }); +}; + + + +(function init(){ + + poolConfigs = buildPoolConfigs(); + + spawnPoolWorkers(); + + startPaymentProcessor(); + + startWebsite(); + + startProfitSwitch(); + + startCliListener(); + +})(); diff --git a/libs/api.js b/libs/api.js new file mode 100644 index 0000000..1b41611 --- /dev/null +++ b/libs/api.js @@ -0,0 +1,135 @@ +var redis = require('redis'); +var async = require('async'); + +var stats = require('./stats.js'); + +module.exports = function(logger, portalConfig, poolConfigs){ + + var _this = this; + + var portalStats = this.stats = new stats(logger, portalConfig, poolConfigs); + + this.liveStatConnections = {}; + + this.handleApiRequest = function(req, res, next){ + + switch(req.params.method){ + case 'stats': + res.header('Content-Type', 'application/json'); + res.end(portalStats.statsString); + return; + case 'pool_stats': + res.header('Content-Type', 'application/json'); + res.end(JSON.stringify(portalStats.statPoolHistory)); + return; + case 'blocks': + case 'getblocksstats': + portalStats.getBlocks(function(data){ + res.header('Content-Type', 'application/json'); + res.end(JSON.stringify(data)); + }); + break; + case 'payments': + var poolBlocks = []; + for(var pool in portalStats.stats.pools) { + poolBlocks.push({name: pool, pending: portalStats.stats.pools[pool].pending, payments: portalStats.stats.pools[pool].payments}); + } + res.header('Content-Type', 'application/json'); + res.end(JSON.stringify(poolBlocks)); + return; + case 'worker_stats': + res.header('Content-Type', 'application/json'); + if (req.url.indexOf("?")>0) { + var url_parms = req.url.split("?"); + if (url_parms.length > 0) { + var history = {}; + var workers = {}; + var address = url_parms[1] || null; + //res.end(portalStats.getWorkerStats(address)); + if (address != null && address.length > 0) { + // make sure it is just the miners address + address = address.split(".")[0]; + // get miners balance along with worker balances + portalStats.getBalanceByAddress(address, function(balances) { + // get current round share total + portalStats.getTotalSharesByAddress(address, function(shares) { + var totalHash = parseFloat(0.0); + var totalShares = shares; + var networkSols = 0; + for (var h in portalStats.statHistory) { + for(var pool in portalStats.statHistory[h].pools) { + for(var w in portalStats.statHistory[h].pools[pool].workers){ + if (w.startsWith(address)) { + if (history[w] == null) { + history[w] = []; + } + if (portalStats.statHistory[h].pools[pool].workers[w].hashrate) { + history[w].push({time: portalStats.statHistory[h].time, hashrate:portalStats.statHistory[h].pools[pool].workers[w].hashrate}); + } + } + } + // order check... + //console.log(portalStats.statHistory[h].time); + } + } + for(var pool in portalStats.stats.pools) { + for(var w in portalStats.stats.pools[pool].workers){ + if (w.startsWith(address)) { + workers[w] = portalStats.stats.pools[pool].workers[w]; + for (var b in balances.balances) { + if (w == balances.balances[b].worker) { + workers[w].paid = balances.balances[b].paid; + workers[w].balance = balances.balances[b].balance; + } + } + workers[w].balance = (workers[w].balance || 0); + workers[w].paid = (workers[w].paid || 0); + totalHash += portalStats.stats.pools[pool].workers[w].hashrate; + networkSols = portalStats.stats.pools[pool].poolStats.networkSols; + } + } + } + res.end(JSON.stringify({miner: address, totalHash: totalHash, totalShares: totalShares, networkSols: networkSols, immature: balances.totalImmature, balance: balances.totalHeld, paid: balances.totalPaid, workers: workers, history: history})); + }); + }); + } else { + res.end(JSON.stringify({result: "error"})); + } + } else { + res.end(JSON.stringify({result: "error"})); + } + } else { + res.end(JSON.stringify({result: "error"})); + } + return; + case 'live_stats': + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + }); + res.write('\n'); + var uid = Math.random().toString(); + _this.liveStatConnections[uid] = res; + res.flush(); + req.on("close", function() { + delete _this.liveStatConnections[uid]; + }); + return; + default: + next(); + } + }; + + this.handleAdminApiRequest = function(req, res, next){ + switch(req.params.method){ + case 'pools': { + res.end(JSON.stringify({result: poolConfigs})); + return; + } + default: + next(); + } + }; + +}; diff --git a/libs/apiBittrex.js b/libs/apiBittrex.js new file mode 100644 index 0000000..9a46d58 --- /dev/null +++ b/libs/apiBittrex.js @@ -0,0 +1,222 @@ +var request = require('request'); +var nonce = require('nonce'); + +module.exports = function() { + 'use strict'; + + // Module dependencies + + // Constants + var version = '0.1.0', + PUBLIC_API_URL = 'https://bittrex.com/api/v1/public', + PRIVATE_API_URL = 'https://bittrex.com/api/v1/market', + USER_AGENT = 'nomp/node-open-mining-portal' + + // Constructor + function Bittrex(key, secret){ + // Generate headers signed by this user's key and secret. + // The secret is encapsulated and never exposed + this._getPrivateHeaders = function(parameters){ + var paramString, signature; + + if (!key || !secret){ + throw 'Bittrex: Error. API key and secret required'; + } + + // Sort parameters alphabetically and convert to `arg1=foo&arg2=bar` + paramString = Object.keys(parameters).sort().map(function(param){ + return encodeURIComponent(param) + '=' + encodeURIComponent(parameters[param]); + }).join('&'); + + signature = crypto.createHmac('sha512', secret).update(paramString).digest('hex'); + + return { + Key: key, + Sign: signature + }; + }; + } + + // If a site uses non-trusted SSL certificates, set this value to false + Bittrex.STRICT_SSL = true; + + // Helper methods + function joinCurrencies(currencyA, currencyB){ + return currencyA + '-' + currencyB; + } + + // Prototype + Bittrex.prototype = { + constructor: Bittrex, + + // Make an API request + _request: function(options, callback){ + if (!('headers' in options)){ + options.headers = {}; + } + + options.headers['User-Agent'] = USER_AGENT; + options.json = true; + options.strictSSL = Bittrex.STRICT_SSL; + + request(options, function(err, response, body) { + callback(err, body); + }); + + return this; + }, + + // Make a public API request + _public: function(parameters, callback){ + var options = { + method: 'GET', + url: PUBLIC_API_URL, + qs: parameters + }; + + return this._request(options, callback); + }, + + // Make a private API request + _private: function(parameters, callback){ + var options; + + parameters.nonce = nonce(); + options = { + method: 'POST', + url: PRIVATE_API_URL, + form: parameters, + headers: this._getPrivateHeaders(parameters) + }; + + return this._request(options, callback); + }, + + + ///// + + + // PUBLIC METHODS + + getTicker: function(callback){ + var options = { + method: 'GET', + url: PUBLIC_API_URL + '/getmarketsummaries', + qs: null + }; + + return this._request(options, callback); + }, + + // getBuyOrderBook: function(currencyA, currencyB, callback){ + // var options = { + // method: 'GET', + // url: PUBLIC_API_URL + '/orders/' + currencyB + '/' + currencyA + '/BUY', + // qs: null + // }; + + // return this._request(options, callback); + // }, + + getOrderBook: function(currencyA, currencyB, callback){ + var parameters = { + market: joinCurrencies(currencyA, currencyB), + type: 'buy', + depth: '50' + } + var options = { + method: 'GET', + url: PUBLIC_API_URL + '/getorderbook', + qs: parameters + } + + return this._request(options, callback); + }, + + getTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + + ///// + + + // PRIVATE METHODS + + myBalances: function(callback){ + var parameters = { + command: 'returnBalances' + }; + + return this._private(parameters, callback); + }, + + myOpenOrders: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOpenOrders', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + myTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + buy: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'buy', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + sell: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'sell', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + cancelOrder: function(currencyA, currencyB, orderNumber, callback){ + var parameters = { + command: 'cancelOrder', + currencyPair: joinCurrencies(currencyA, currencyB), + orderNumber: orderNumber + }; + + return this._private(parameters, callback); + }, + + withdraw: function(currency, amount, address, callback){ + var parameters = { + command: 'withdraw', + currency: currency, + amount: amount, + address: address + }; + + return this._private(parameters, callback); + } + }; + + return Bittrex; +}(); diff --git a/libs/apiCoinWarz.js b/libs/apiCoinWarz.js new file mode 100644 index 0000000..172d2b7 --- /dev/null +++ b/libs/apiCoinWarz.js @@ -0,0 +1,115 @@ +var request = require('request'); +var nonce = require('nonce'); + +module.exports = function() { + 'use strict'; + + // Module dependencies + + // Constants + var version = '0.0.1', + PUBLIC_API_URL = 'http://www.coinwarz.com/v1/api/profitability/?apikey=YOUR_API_KEY&algo=all', + USER_AGENT = 'nomp/node-open-mining-portal' + + // Constructor + function Cryptsy(key, secret){ + // Generate headers signed by this user's key and secret. + // The secret is encapsulated and never exposed + this._getPrivateHeaders = function(parameters){ + var paramString, signature; + + if (!key || !secret){ + throw 'CoinWarz: Error. API key and secret required'; + } + + // Sort parameters alphabetically and convert to `arg1=foo&arg2=bar` + paramString = Object.keys(parameters).sort().map(function(param){ + return encodeURIComponent(param) + '=' + encodeURIComponent(parameters[param]); + }).join('&'); + + signature = crypto.createHmac('sha512', secret).update(paramString).digest('hex'); + + return { + Key: key, + Sign: signature + }; + }; + } + + // If a site uses non-trusted SSL certificates, set this value to false + Cryptsy.STRICT_SSL = true; + + // Helper methods + function joinCurrencies(currencyA, currencyB){ + return currencyA + '_' + currencyB; + } + + // Prototype + CoinWarz.prototype = { + constructor: CoinWarz, + + // Make an API request + _request: function(options, callback){ + if (!('headers' in options)){ + options.headers = {}; + } + + options.headers['User-Agent'] = USER_AGENT; + options.json = true; + options.strictSSL = CoinWarz.STRICT_SSL; + + request(options, function(err, response, body) { + callback(err, body); + }); + + return this; + }, + + // Make a public API request + _public: function(parameters, callback){ + var options = { + method: 'GET', + url: PUBLIC_API_URL, + qs: parameters + }; + + return this._request(options, callback); + }, + + + ///// + + + // PUBLIC METHODS + + getTicker: function(callback){ + var parameters = { + method: 'marketdatav2' + }; + + return this._public(parameters, callback); + }, + + getOrderBook: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOrderBook', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + getTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + + //// + + return CoinWarz; +}(); diff --git a/libs/apiCryptsy.js b/libs/apiCryptsy.js new file mode 100644 index 0000000..6563ab5 --- /dev/null +++ b/libs/apiCryptsy.js @@ -0,0 +1,204 @@ +var request = require('request'); +var nonce = require('nonce'); + +module.exports = function() { + 'use strict'; + + // Module dependencies + + // Constants + var version = '0.1.0', + PUBLIC_API_URL = 'http://pubapi.cryptsy.com/api.php', + PRIVATE_API_URL = 'https://api.cryptsy.com/api', + USER_AGENT = 'nomp/node-open-mining-portal' + + // Constructor + function Cryptsy(key, secret){ + // Generate headers signed by this user's key and secret. + // The secret is encapsulated and never exposed + this._getPrivateHeaders = function(parameters){ + var paramString, signature; + + if (!key || !secret){ + throw 'Cryptsy: Error. API key and secret required'; + } + + // Sort parameters alphabetically and convert to `arg1=foo&arg2=bar` + paramString = Object.keys(parameters).sort().map(function(param){ + return encodeURIComponent(param) + '=' + encodeURIComponent(parameters[param]); + }).join('&'); + + signature = crypto.createHmac('sha512', secret).update(paramString).digest('hex'); + + return { + Key: key, + Sign: signature + }; + }; + } + + // If a site uses non-trusted SSL certificates, set this value to false + Cryptsy.STRICT_SSL = true; + + // Helper methods + function joinCurrencies(currencyA, currencyB){ + return currencyA + '_' + currencyB; + } + + // Prototype + Cryptsy.prototype = { + constructor: Cryptsy, + + // Make an API request + _request: function(options, callback){ + if (!('headers' in options)){ + options.headers = {}; + } + + options.headers['User-Agent'] = USER_AGENT; + options.json = true; + options.strictSSL = Cryptsy.STRICT_SSL; + + request(options, function(err, response, body) { + callback(err, body); + }); + + return this; + }, + + // Make a public API request + _public: function(parameters, callback){ + var options = { + method: 'GET', + url: PUBLIC_API_URL, + qs: parameters + }; + + return this._request(options, callback); + }, + + // Make a private API request + _private: function(parameters, callback){ + var options; + + parameters.nonce = nonce(); + options = { + method: 'POST', + url: PRIVATE_API_URL, + form: parameters, + headers: this._getPrivateHeaders(parameters) + }; + + return this._request(options, callback); + }, + + + ///// + + + // PUBLIC METHODS + + getTicker: function(callback){ + var parameters = { + method: 'marketdatav2' + }; + + return this._public(parameters, callback); + }, + + getOrderBook: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOrderBook', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + getTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + + ///// + + + // PRIVATE METHODS + + myBalances: function(callback){ + var parameters = { + command: 'returnBalances' + }; + + return this._private(parameters, callback); + }, + + myOpenOrders: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOpenOrders', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + myTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + buy: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'buy', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + sell: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'sell', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + cancelOrder: function(currencyA, currencyB, orderNumber, callback){ + var parameters = { + command: 'cancelOrder', + currencyPair: joinCurrencies(currencyA, currencyB), + orderNumber: orderNumber + }; + + return this._private(parameters, callback); + }, + + withdraw: function(currency, amount, address, callback){ + var parameters = { + command: 'withdraw', + currency: currency, + amount: amount, + address: address + }; + + return this._private(parameters, callback); + } + }; + + return Cryptsy; +}(); diff --git a/libs/apiMintpal.js b/libs/apiMintpal.js new file mode 100644 index 0000000..fec22e6 --- /dev/null +++ b/libs/apiMintpal.js @@ -0,0 +1,216 @@ +var request = require('request'); +var nonce = require('nonce'); + +module.exports = function() { + 'use strict'; + + // Module dependencies + + // Constants + var version = '0.1.0', + PUBLIC_API_URL = 'https://api.mintpal.com/v2/market', + PRIVATE_API_URL = 'https://api.mintpal.com/v2/market', + USER_AGENT = 'nomp/node-open-mining-portal' + + // Constructor + function Mintpal(key, secret){ + // Generate headers signed by this user's key and secret. + // The secret is encapsulated and never exposed + this._getPrivateHeaders = function(parameters){ + var paramString, signature; + + if (!key || !secret){ + throw 'Mintpal: Error. API key and secret required'; + } + + // Sort parameters alphabetically and convert to `arg1=foo&arg2=bar` + paramString = Object.keys(parameters).sort().map(function(param){ + return encodeURIComponent(param) + '=' + encodeURIComponent(parameters[param]); + }).join('&'); + + signature = crypto.createHmac('sha512', secret).update(paramString).digest('hex'); + + return { + Key: key, + Sign: signature + }; + }; + } + + // If a site uses non-trusted SSL certificates, set this value to false + Mintpal.STRICT_SSL = true; + + // Helper methods + function joinCurrencies(currencyA, currencyB){ + return currencyA + '_' + currencyB; + } + + // Prototype + Mintpal.prototype = { + constructor: Mintpal, + + // Make an API request + _request: function(options, callback){ + if (!('headers' in options)){ + options.headers = {}; + } + + options.headers['User-Agent'] = USER_AGENT; + options.json = true; + options.strictSSL = Mintpal.STRICT_SSL; + + request(options, function(err, response, body) { + callback(err, body); + }); + + return this; + }, + + // Make a public API request + _public: function(parameters, callback){ + var options = { + method: 'GET', + url: PUBLIC_API_URL, + qs: parameters + }; + + return this._request(options, callback); + }, + + // Make a private API request + _private: function(parameters, callback){ + var options; + + parameters.nonce = nonce(); + options = { + method: 'POST', + url: PRIVATE_API_URL, + form: parameters, + headers: this._getPrivateHeaders(parameters) + }; + + return this._request(options, callback); + }, + + + ///// + + + // PUBLIC METHODS + + getTicker: function(callback){ + var options = { + method: 'GET', + url: PUBLIC_API_URL + '/summary', + qs: null + }; + + return this._request(options, callback); + }, + + getBuyOrderBook: function(currencyA, currencyB, callback){ + var options = { + method: 'GET', + url: PUBLIC_API_URL + '/orders/' + currencyB + '/' + currencyA + '/BUY', + qs: null + }; + + return this._request(options, callback); + }, + + getOrderBook: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOrderBook', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + getTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + + ///// + + + // PRIVATE METHODS + + myBalances: function(callback){ + var parameters = { + command: 'returnBalances' + }; + + return this._private(parameters, callback); + }, + + myOpenOrders: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOpenOrders', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + myTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + buy: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'buy', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + sell: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'sell', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + cancelOrder: function(currencyA, currencyB, orderNumber, callback){ + var parameters = { + command: 'cancelOrder', + currencyPair: joinCurrencies(currencyA, currencyB), + orderNumber: orderNumber + }; + + return this._private(parameters, callback); + }, + + withdraw: function(currency, amount, address, callback){ + var parameters = { + command: 'withdraw', + currency: currency, + amount: amount, + address: address + }; + + return this._private(parameters, callback); + } + }; + + return Mintpal; +}(); diff --git a/libs/apiPoloniex.js b/libs/apiPoloniex.js new file mode 100644 index 0000000..fc483e1 --- /dev/null +++ b/libs/apiPoloniex.js @@ -0,0 +1,212 @@ +var request = require('request'); +var nonce = require('nonce'); + +module.exports = function() { + 'use strict'; + + // Module dependencies + + // Constants + var version = '0.1.0', + PUBLIC_API_URL = 'https://poloniex.com/public', + PRIVATE_API_URL = 'https://poloniex.com/tradingApi', + USER_AGENT = 'npm-crypto-apis/' + version + + // Constructor + function Poloniex(key, secret){ + // Generate headers signed by this user's key and secret. + // The secret is encapsulated and never exposed + this._getPrivateHeaders = function(parameters){ + var paramString, signature; + + if (!key || !secret){ + throw 'Poloniex: Error. API key and secret required'; + } + + // Sort parameters alphabetically and convert to `arg1=foo&arg2=bar` + paramString = Object.keys(parameters).sort().map(function(param){ + return encodeURIComponent(param) + '=' + encodeURIComponent(parameters[param]); + }).join('&'); + + signature = crypto.createHmac('sha512', secret).update(paramString).digest('hex'); + + return { + Key: key, + Sign: signature + }; + }; + } + + // If a site uses non-trusted SSL certificates, set this value to false + Poloniex.STRICT_SSL = true; + + // Helper methods + function joinCurrencies(currencyA, currencyB){ + return currencyA + '_' + currencyB; + } + + // Prototype + Poloniex.prototype = { + constructor: Poloniex, + + // Make an API request + _request: function(options, callback){ + if (!('headers' in options)){ + options.headers = {}; + } + + options.headers['User-Agent'] = USER_AGENT; + options.json = true; + options.strictSSL = Poloniex.STRICT_SSL; + + request(options, function(err, response, body) { + callback(err, body); + }); + + return this; + }, + + // Make a public API request + _public: function(parameters, callback){ + var options = { + method: 'GET', + url: PUBLIC_API_URL, + qs: parameters + }; + + return this._request(options, callback); + }, + + // Make a private API request + _private: function(parameters, callback){ + var options; + + parameters.nonce = nonce(); + options = { + method: 'POST', + url: PRIVATE_API_URL, + form: parameters, + headers: this._getPrivateHeaders(parameters) + }; + + return this._request(options, callback); + }, + + + ///// + + + // PUBLIC METHODS + + getTicker: function(callback){ + var parameters = { + command: 'returnTicker' + }; + + return this._public(parameters, callback); + }, + + get24hVolume: function(callback){ + var parameters = { + command: 'return24hVolume' + }; + + return this._public(parameters, callback); + }, + + getOrderBook: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOrderBook', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + getTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + + ///// + + + // PRIVATE METHODS + + myBalances: function(callback){ + var parameters = { + command: 'returnBalances' + }; + + return this._private(parameters, callback); + }, + + myOpenOrders: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOpenOrders', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + myTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + buy: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'buy', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + sell: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'sell', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + cancelOrder: function(currencyA, currencyB, orderNumber, callback){ + var parameters = { + command: 'cancelOrder', + currencyPair: joinCurrencies(currencyA, currencyB), + orderNumber: orderNumber + }; + + return this._private(parameters, callback); + }, + + withdraw: function(currency, amount, address, callback){ + var parameters = { + command: 'withdraw', + currency: currency, + amount: amount, + address: address + }; + + return this._private(parameters, callback); + } + }; + + return Poloniex; +}(); diff --git a/libs/cliListener.js b/libs/cliListener.js new file mode 100644 index 0000000..aa4a0e9 --- /dev/null +++ b/libs/cliListener.js @@ -0,0 +1,45 @@ +var events = require('events'); +var net = require('net'); + +var listener = module.exports = function listener(port){ + + var _this = this; + + var emitLog = function(text){ + _this.emit('log', text); + }; + + + this.start = function(){ + net.createServer(function(c) { + + var data = ''; + try { + c.on('data', function (d) { + data += d; + if (data.slice(-1) === '\n') { + var message = JSON.parse(data); + _this.emit('command', message.command, message.params, message.options, function(message){ + c.end(message); + }); + } + }); + c.on('end', function () { + + }); + c.on('error', function () { + + }); + } + catch(e){ + emitLog('CLI listener failed to parse message ' + data); + } + + }).listen(port, '127.0.0.1', function() { + emitLog('CLI listening on port ' + port) + }); + } + +}; + +listener.prototype.__proto__ = events.EventEmitter.prototype; diff --git a/libs/logUtil.js b/libs/logUtil.js new file mode 100644 index 0000000..7b56b98 --- /dev/null +++ b/libs/logUtil.js @@ -0,0 +1,89 @@ +var dateFormat = require('dateformat'); +var colors = require('colors'); + + +var severityToColor = function(severity, text) { + switch(severity) { + case 'special': + return text.cyan.underline; + case 'debug': + return text.green; + case 'warning': + return text.yellow; + case 'error': + return text.red; + default: + console.log("Unknown severity " + severity); + return text.italic; + } +}; + +var severityValues = { + 'debug': 1, + 'warning': 2, + 'error': 3, + 'special': 4 +}; + + +var PoolLogger = function (configuration) { + + + var logLevelInt = severityValues[configuration.logLevel]; + var logColors = configuration.logColors; + + + + var log = function(severity, system, component, text, subcat) { + + if (severityValues[severity] < logLevelInt) return; + + if (subcat){ + var realText = subcat; + var realSubCat = text; + text = realText; + subcat = realSubCat; + } + + var entryDesc = dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss') + ' [' + system + ']\t'; + if (logColors) { + entryDesc = severityToColor(severity, entryDesc); + + var logString = + entryDesc + + ('[' + component + '] ').italic; + + if (subcat) + logString += ('(' + subcat + ') ').bold.grey; + + logString += text.grey; + } + else { + var logString = + entryDesc + + '[' + component + '] '; + + if (subcat) + logString += '(' + subcat + ') '; + + logString += text; + } + + console.log(logString); + + + }; + + // public + + var _this = this; + Object.keys(severityValues).forEach(function(logType){ + _this[logType] = function(){ + var args = Array.prototype.slice.call(arguments, 0); + args.unshift(logType); + log.apply(this, args); + }; + }); +}; + +module.exports = PoolLogger; \ No newline at end of file diff --git a/libs/mposCompatibility.js b/libs/mposCompatibility.js new file mode 100644 index 0000000..6f044c4 --- /dev/null +++ b/libs/mposCompatibility.js @@ -0,0 +1,122 @@ +var mysql = require('mysql'); +var cluster = require('cluster'); +module.exports = function(logger, poolConfig){ + + var mposConfig = poolConfig.mposMode; + var coin = poolConfig.coin.name; + + var connection = mysql.createPool({ + host: mposConfig.host, + port: mposConfig.port, + user: mposConfig.user, + password: mposConfig.password, + database: mposConfig.database + }); + + + var logIdentify = 'MySQL'; + var logComponent = coin; + + + + this.handleAuth = function(workerName, password, authCallback){ + + if (poolConfig.validateWorkerUsername !== true && mposConfig.autoCreateWorker !== true){ + authCallback(true); + return; + } + + connection.query( + 'SELECT password FROM pool_worker WHERE username = LOWER(?)', + [workerName.toLowerCase()], + function(err, result){ + if (err){ + logger.error(logIdentify, logComponent, 'Database error when authenticating worker: ' + + JSON.stringify(err)); + authCallback(false); + } + else if (!result[0]){ + if(mposConfig.autoCreateWorker){ + var account = workerName.split('.')[0]; + connection.query( + 'SELECT id,username FROM accounts WHERE username = LOWER(?)', + [account.toLowerCase()], + function(err, result){ + if (err){ + logger.error(logIdentify, logComponent, 'Database error when authenticating account: ' + + JSON.stringify(err)); + authCallback(false); + }else if(!result[0]){ + authCallback(false); + }else{ + connection.query( + "INSERT INTO `pool_worker` (`account_id`, `username`, `password`) VALUES (?, ?, ?);", + [result[0].id,workerName.toLowerCase(),password], + function(err, result){ + if (err){ + logger.error(logIdentify, logComponent, 'Database error when insert worker: ' + + JSON.stringify(err)); + authCallback(false); + }else { + authCallback(true); + } + }) + } + } + ); + } + else{ + authCallback(false); + } + } + else if (mposConfig.checkPassword && result[0].password !== password) + authCallback(false); + else + authCallback(true); + } + ); + + }; + + this.handleShare = function(isValidShare, isValidBlock, shareData){ + + var dbData = [ + shareData.ip, + shareData.worker, + isValidShare ? 'Y' : 'N', + isValidBlock ? 'Y' : 'N', + shareData.difficulty * (poolConfig.coin.mposDiffMultiplier || 1), + typeof(shareData.error) === 'undefined' ? null : shareData.error, + shareData.blockHash ? shareData.blockHash : (shareData.blockHashInvalid ? shareData.blockHashInvalid : '') + ]; + connection.query( + 'INSERT INTO `shares` SET time = NOW(), rem_host = ?, username = ?, our_result = ?, upstream_result = ?, difficulty = ?, reason = ?, solution = ?', + dbData, + function(err, result) { + if (err) + logger.error(logIdentify, logComponent, 'Insert error when adding share: ' + JSON.stringify(err)); + else + logger.debug(logIdentify, logComponent, 'Share inserted'); + } + ); + }; + + this.handleDifficultyUpdate = function(workerName, diff){ + + connection.query( + 'UPDATE `pool_worker` SET `difficulty` = ' + diff + ' WHERE `username` = ' + connection.escape(workerName), + function(err, result){ + if (err) + logger.error(logIdentify, logComponent, 'Error when updating worker diff: ' + + JSON.stringify(err)); + else if (result.affectedRows === 0){ + connection.query('INSERT INTO `pool_worker` SET ?', {username: workerName, difficulty: diff}); + } + else + console.log('Updated difficulty successfully', result); + } + ); + }; + + +}; diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js new file mode 100644 index 0000000..5e24b4d --- /dev/null +++ b/libs/paymentProcessor.js @@ -0,0 +1,1426 @@ +var fs = require('fs'); +var request = require('request'); + +var redis = require('redis'); +var async = require('async'); + +var Stratum = require('stratum-pool'); +var util = require('stratum-pool/lib/util.js'); + +module.exports = function(logger){ + + var poolConfigs = JSON.parse(process.env.pools); + + var enabledPools = []; + + Object.keys(poolConfigs).forEach(function(coin) { + var poolOptions = poolConfigs[coin]; + if (poolOptions.paymentProcessing && + poolOptions.paymentProcessing.enabled) + enabledPools.push(coin); + }); + + async.filter(enabledPools, function(coin, callback){ + SetupForPool(logger, poolConfigs[coin], function(setupResults){ + callback(null, setupResults); + }); + }, function(err, results){ + results.forEach(function(coin){ + + var poolOptions = poolConfigs[coin]; + var processingConfig = poolOptions.paymentProcessing; + var logSystem = 'Payments'; + var logComponent = coin; + + logger.debug(logSystem, logComponent, 'Payment processing setup with daemon (' + + processingConfig.daemon.user + '@' + processingConfig.daemon.host + ':' + processingConfig.daemon.port + + ') and redis (' + poolOptions.redis.host + ':' + poolOptions.redis.port + ')'); + }); + }); +}; + +function SetupForPool(logger, poolOptions, setupFinished){ + + + var coin = poolOptions.coin.name; + var processingConfig = poolOptions.paymentProcessing; + + var logSystem = 'Payments'; + var logComponent = coin; + + var opidCount = 0; + var opids = []; + + // zcash team recommends 10 confirmations for safety from orphaned blocks + var minConfShield = Math.max((processingConfig.minConf || 10), 1); // Don't allow 0 conf transactions. + var minConfPayout = Math.max((processingConfig.minConf || 10), 1); + if (minConfPayout < 3) { + logger.warning(logSystem, logComponent, logComponent + ' minConf of 3 is recommended.'); + } + + // minimum paymentInterval of 60 seconds + var paymentIntervalSecs = Math.max((processingConfig.paymentInterval || 120), 30); + if (parseInt(processingConfig.paymentInterval) < 120) { + logger.warning(logSystem, logComponent, ' minimum paymentInterval of 120 seconds recommended.'); + } + + var maxBlocksPerPayment = Math.max(processingConfig.maxBlocksPerPayment || 3, 1); + + // pplnt - pay per last N time shares + var pplntEnabled = processingConfig.paymentMode === "pplnt" || false; + var pplntTimeQualify = processingConfig.pplnt || 0.51; // 51% + + var getMarketStats = poolOptions.coin.getMarketStats === true; + var requireShielding = poolOptions.coin.requireShielding === true; + var fee = parseFloat(poolOptions.coin.txfee) || parseFloat(0.0004); + + logger.debug(logSystem, logComponent, logComponent + ' requireShielding: ' + requireShielding); + logger.debug(logSystem, logComponent, logComponent + ' minConf: ' + minConfShield); + logger.debug(logSystem, logComponent, logComponent + ' payments txfee reserve: ' + fee); + logger.debug(logSystem, logComponent, logComponent + ' maxBlocksPerPayment: ' + maxBlocksPerPayment); + logger.debug(logSystem, logComponent, logComponent + ' PPLNT: ' + pplntEnabled + ', time period: '+pplntTimeQualify); + + var daemon = new Stratum.daemon.interface([processingConfig.daemon], function(severity, message){ + logger[severity](logSystem, logComponent, message); + }); + var redisClient = redis.createClient(poolOptions.redis.port, poolOptions.redis.host); + // redis auth if enabled + if (poolOptions.redis.password) { + redisClient.auth(poolOptions.redis.password); + } + + var magnitude; + var minPaymentSatoshis; + var coinPrecision; + + var paymentInterval; + + function validateAddress (callback){ + daemon.cmd('validateaddress', [poolOptions.address], function(result) { + if (result.error){ + logger.error(logSystem, logComponent, 'Error with payment processing daemon ' + JSON.stringify(result.error)); + callback(true); + } + else if (!result.response || !result.response.ismine) { + logger.error(logSystem, logComponent, + 'Daemon does not own pool address - payment processing can not be done with this daemon, ' + + JSON.stringify(result.response)); + callback(true); + } + else{ + callback() + } + }, true); + } + function validateTAddress (callback) { + daemon.cmd('validateaddress', [poolOptions.tAddress], function(result) { + if (result.error){ + logger.error(logSystem, logComponent, 'Error with payment processing daemon ' + JSON.stringify(result.error)); + callback(true); + } + else if (!result.response || !result.response.ismine) { + logger.error(logSystem, logComponent, + 'Daemon does not own pool address - payment processing can not be done with this daemon, ' + + JSON.stringify(result.response)); + callback(true); + } + else{ + callback() + } + }, true); + } + function validateZAddress (callback) { + daemon.cmd('z_validateaddress', [poolOptions.zAddress], function(result) { + if (result.error){ + logger.error(logSystem, logComponent, 'Error with payment processing daemon ' + JSON.stringify(result.error)); + callback(true); + } + else if (!result.response || !result.response.ismine) { + logger.error(logSystem, logComponent, + 'Daemon does not own pool address - payment processing can not be done with this daemon, ' + + JSON.stringify(result.response)); + callback(true); + } + else{ + callback() + } + }, true); + } + function getBalance(callback){ + daemon.cmd('getbalance', [], function(result){ + if (result.error){ + return callback(true); + } + try { + var d = result.data.split('result":')[1].split(',')[0].split('.')[1]; + magnitude = parseInt('10' + new Array(d.length).join('0')); + minPaymentSatoshis = parseInt(processingConfig.minimumPayment * magnitude); + coinPrecision = magnitude.toString().length - 1; + } + catch(e){ + logger.error(logSystem, logComponent, 'Error detecting number of satoshis in a coin, cannot do payment processing. Tried parsing: ' + result.data); + return callback(true); + } + callback(); + }, true, true); + } + + function asyncComplete(err){ + if (err){ + setupFinished(false); + return; + } + if (paymentInterval) { + clearInterval(paymentInterval); + } + paymentInterval = setInterval(processPayments, paymentIntervalSecs * 1000); + //setTimeout(processPayments, 100); + setupFinished(true); + } + + if (requireShielding === true) { + async.parallel([validateAddress, validateTAddress, validateZAddress, getBalance], asyncComplete); + } else { + async.parallel([validateAddress, validateTAddress, getBalance], asyncComplete); + } + + //get t_address coinbalance + function listUnspent (addr, notAddr, minConf, displayBool, callback) { + if (addr !== null) { + var args = [minConf, 99999999, [addr]]; + } else { + addr = 'Payout wallet'; + var args = [minConf, 99999999]; + } + daemon.cmd('listunspent', args, function (result) { + if (!result || result.error || result[0].error) { + logger.error(logSystem, logComponent, 'Error with RPC call listunspent '+addr+' '+JSON.stringify(result[0].error)); + callback = function (){}; + callback(true); + } + else { + var tBalance = parseFloat(0); + if (result[0].response != null && result[0].response.length > 0) { + for (var i = 0, len = result[0].response.length; i < len; i++) { + if (result[0].response[i].address && result[0].response[i].address !== notAddr) { + tBalance += parseFloat(result[0].response[i].amount || 0); + } + } + tBalance = coinsRound(tBalance); + } + if (displayBool === true) { + logger.special(logSystem, logComponent, addr+' balance of ' + tBalance); + } + callback(null, coinsToSatoshies(tBalance)); + } + }); + } + + // get z_address coinbalance + function listUnspentZ (addr, minConf, displayBool, callback) { + daemon.cmd('z_getbalance', [addr, minConf], function (result) { + if (!result || result.error || result[0].error) { + logger.error(logSystem, logComponent, 'Error with RPC call z_getbalance '+addr+' '+JSON.stringify(result[0].error)); + callback = function (){}; + callback(true); + } + else { + var zBalance = parseFloat(0); + if (result[0].response != null) { + zBalance = coinsRound(result[0].response); + } + if (displayBool === true) { + logger.special(logSystem, logComponent, addr.substring(0,14) + '...' + addr.substring(addr.length - 14) + ' balance: '+(zBalance).toFixed(8)); + } + callback(null, coinsToSatoshies(zBalance)); + } + }); + } + + //send t_address balance to z_address + function sendTToZ (callback, tBalance) { + if (callback === true) + return; + if (tBalance === NaN) { + logger.error(logSystem, logComponent, 'tBalance === NaN for sendTToZ'); + return; + } + if ((tBalance - 10000) <= 0) + return; + + // do not allow more than a single z_sendmany operation at a time + if (opidCount > 0) { + logger.warning(logSystem, logComponent, 'sendTToZ is waiting, too many z_sendmany operations already in progress.'); + return; + } + + var amount = satoshisToCoins(tBalance - 10000); + var params = [poolOptions.address, [{'address': poolOptions.zAddress, 'amount': amount}]]; + daemon.cmd('z_sendmany', params, + function (result) { + //Check if payments failed because wallet doesn't have enough coins to pay for tx fees + if (!result || result.error || result[0].error || !result[0].response) { + logger.error(logSystem, logComponent, 'Error trying to shield balance '+amount+' '+JSON.stringify(result[0].error)); + callback = function (){}; + callback(true); + } + else { + var opid = (result.response || result[0].response); + opidCount++; + opids.push(opid); + logger.special(logSystem, logComponent, 'Shield balance ' + amount + ' ' + opid); + callback = function (){}; + callback(null); + } + } + ); + } + + // send z_address balance to t_address + function sendZToT (callback, zBalance) { + if (callback === true) + return; + if (zBalance === NaN) { + logger.error(logSystem, logComponent, 'zBalance === NaN for sendZToT'); + return; + } + if ((zBalance - 10000) <= 0) + return; + + // do not allow more than a single z_sendmany operation at a time + if (opidCount > 0) { + logger.warning(logSystem, logComponent, 'sendZToT is waiting, too many z_sendmany operations already in progress.'); + return; + } + + var amount = satoshisToCoins(zBalance - 10000); + // unshield no more than 100 ZEC at a time + if (amount > 100.0) + amount = 100.0; + + var params = [poolOptions.zAddress, [{'address': poolOptions.tAddress, 'amount': amount}]]; + daemon.cmd('z_sendmany', params, + function (result) { + //Check if payments failed because wallet doesn't have enough coins to pay for tx fees + if (!result || result.error || result[0].error || !result[0].response) { + logger.error(logSystem, logComponent, 'Error trying to send z_address coin balance to payout t_address.'+JSON.stringify(result[0].error)); + callback = function (){}; + callback(true); + } + else { + var opid = (result.response || result[0].response); + opidCount++; + opids.push(opid); + logger.special(logSystem, logComponent, 'Unshield funds for payout ' + amount + ' ' + opid); + callback = function (){}; + callback(null); + } + } + ); + } + + function cacheMarketStats() { + var marketStatsUpdate = []; + var coin = logComponent.replace('_testnet', '').toLowerCase(); + if (coin == 'zen') + coin = 'zencash'; + + request('https://api.coinmarketcap.com/v1/ticker/'+coin+'/', function (error, response, body) { + if (error) { + logger.error(logSystem, logComponent, 'Error with http request to https://api.coinmarketcap.com/ ' + JSON.stringify(error)); + return; + } + if (response && response.statusCode) { + if (response.statusCode == 200) { + if (body) { + var data = JSON.parse(body); + if (data.length > 0) { + marketStatsUpdate.push(['hset', logComponent + ':stats', 'coinmarketcap', JSON.stringify(data)]); + redisClient.multi(marketStatsUpdate).exec(function(err, results){ + if (err){ + logger.error(logSystem, logComponent, 'Error with redis during call to cacheMarketStats() ' + JSON.stringify(error)); + return; + } + }); + } + } + } else { + logger.error(logSystem, logComponent, 'Error, unexpected http status code during call to cacheMarketStats() ' + JSON.stringify(response.statusCode)); + } + } + }); + } + + function cacheNetworkStats () { + var params = null; + daemon.cmd('getmininginfo', params, + function (result) { + if (!result || result.error || result[0].error || !result[0].response) { + logger.error(logSystem, logComponent, 'Error with RPC call getmininginfo '+JSON.stringify(result[0].error)); + return; + } + + var coin = logComponent; + var finalRedisCommands = []; + + if (result[0].response.blocks !== null) { + finalRedisCommands.push(['hset', coin + ':stats', 'networkBlocks', result[0].response.blocks]); + } + if (result[0].response.difficulty !== null) { + finalRedisCommands.push(['hset', coin + ':stats', 'networkDiff', result[0].response.difficulty]); + } + if (result[0].response.networkhashps !== null) { + finalRedisCommands.push(['hset', coin + ':stats', 'networkSols', result[0].response.networkhashps]); + } + + daemon.cmd('getnetworkinfo', params, + function (result) { + if (!result || result.error || result[0].error || !result[0].response) { + logger.error(logSystem, logComponent, 'Error with RPC call getnetworkinfo '+JSON.stringify(result[0].error)); + return; + } + + if (result[0].response.connections !== null) { + finalRedisCommands.push(['hset', coin + ':stats', 'networkConnections', result[0].response.connections]); + } + if (result[0].response.version !== null) { + finalRedisCommands.push(['hset', coin + ':stats', 'networkVersion', result[0].response.version]); + } + if (result[0].response.subversion !== null) { + finalRedisCommands.push(['hset', coin + ':stats', 'networkSubVersion', result[0].response.subversion]); + } + if (result[0].response.protocolversion !== null) { + finalRedisCommands.push(['hset', coin + ':stats', 'networkProtocolVersion', result[0].response.protocolversion]); + } + + if (finalRedisCommands.length <= 0) + return; + + redisClient.multi(finalRedisCommands).exec(function(error, results){ + if (error){ + logger.error(logSystem, logComponent, 'Error with redis during call to cacheNetworkStats() ' + JSON.stringify(error)); + return; + } + }); + } + ); + } + ); + } + + // run shielding process every x minutes + var shieldIntervalState = 0; // do not send ZtoT and TtoZ and same time, this results in operation failed! + var shielding_interval = Math.max(parseInt(poolOptions.walletInterval || 1), 1) * 60 * 1000; // run every x minutes + // shielding not required for some equihash coins + if (requireShielding === true) { + var shieldInterval = setInterval(function() { + shieldIntervalState++; + switch (shieldIntervalState) { + case 1: + listUnspent(poolOptions.address, null, minConfShield, false, sendTToZ); + break; + default: + listUnspentZ(poolOptions.zAddress, minConfShield, false, sendZToT); + shieldIntervalState = 0; + break; + } + }, shielding_interval); + } + + // network stats caching every 58 seconds + var stats_interval = 58 * 1000; + var statsInterval = setInterval(function() { + // update network stats using coin daemon + cacheNetworkStats(); + }, stats_interval); + + // market stats caching every 5 minutes + if (getMarketStats === true) { + var market_stats_interval = 300 * 1000; + var marketStatsInterval = setInterval(function() { + // update market stats using coinmarketcap + cacheMarketStats(); + }, market_stats_interval); + } + + // check operation statuses every 57 seconds + var opid_interval = 57 * 1000; + // shielding not required for some equihash coins + if (requireShielding === true) { + var checkOpids = function() { + clearTimeout(opidTimeout); + var checkOpIdSuccessAndGetResult = function(ops) { + var batchRPC = []; + // if there are no op-ids + if (ops.length == 0) { + // and we think there is + if (opidCount !== 0) { + // clear them! + opidCount = 0; + opids = []; + logger.warning(logSystem, logComponent, 'Clearing operation ids due to empty result set.'); + } + } + // loop through op-ids checking their status + ops.forEach(function(op, i){ + // check operation id status + if (op.status == "success" || op.status == "failed") { + // clear operation id result + var opid_index = opids.indexOf(op.id); + if (opid_index > -1) { + // clear operation id count + batchRPC.push(['z_getoperationresult', [[op.id]]]); + opidCount--; + opids.splice(opid_index, 1); + } + // log status to console + if (op.status == "failed") { + if (op.error) { + logger.error(logSystem, logComponent, "Shielding operation failed " + op.id + " " + op.error.code +", " + op.error.message); + } else { + logger.error(logSystem, logComponent, "Shielding operation failed " + op.id); + } + } else { + logger.special(logSystem, logComponent, 'Shielding operation success ' + op.id + ' txid: ' + op.result.txid); + } + } else if (op.status == "executing") { + logger.special(logSystem, logComponent, 'Shielding operation in progress ' + op.id ); + } + }); + // if there are no completed operations + if (batchRPC.length <= 0) { + opidTimeout = setTimeout(checkOpids, opid_interval); + return; + } + // clear results for completed operations + daemon.batchCmd(batchRPC, function(error, results){ + if (error || !results) { + opidTimeout = setTimeout(checkOpids, opid_interval); + logger.error(logSystem, logComponent, 'Error with RPC call z_getoperationresult ' + JSON.stringify(error)); + return; + } + // check result execution_secs vs pool_config + results.forEach(function(result, i) { + if (result.result[i] && parseFloat(result.result[i].execution_secs || 0) > shielding_interval) { + logger.warning(logSystem, logComponent, 'Warning, walletInverval shorter than opid execution time of '+result.result[i].execution_secs+' secs.'); + } + }); + // keep checking operation ids + opidTimeout = setTimeout(checkOpids, opid_interval); + }); + }; + // check for completed operation ids + daemon.cmd('z_getoperationstatus', null, function (result) { + var err = false; + if (result.error) { + err = true; + logger.error(logSystem, logComponent, 'Error with RPC call z_getoperationstatus ' + JSON.stringify(result.error)); + } else if (result.response) { + checkOpIdSuccessAndGetResult(result.response); + } else { + err = true; + logger.error(logSystem, logComponent, 'No response from z_getoperationstatus RPC call.'); + } + if (err === true) { + opidTimeout = setTimeout(checkOpids, opid_interval); + if (opidCount !== 0) { + opidCount = 0; + opids = []; + logger.warning(logSystem, logComponent, 'Clearing operation ids due to RPC call errors.'); + } + } + }, true, true); + } + var opidTimeout = setTimeout(checkOpids, opid_interval); + } + + function roundTo(n, digits) { + if (digits === undefined) { + digits = 0; + } + var multiplicator = Math.pow(10, digits); + n = parseFloat((n * multiplicator).toFixed(11)); + var test =(Math.round(n) / multiplicator); + return +(test.toFixed(digits)); + } + + var satoshisToCoins = function(satoshis){ + return roundTo((satoshis / magnitude), coinPrecision); + }; + + var coinsToSatoshies = function(coins){ + return Math.round(coins * magnitude); + }; + + function coinsRound(number) { + return roundTo(number, coinPrecision); + } + + function checkForDuplicateBlockHeight(rounds, height) { + var count = 0; + for (var i = 0; i < rounds.length; i++) { + if (rounds[i].height == height) + count++; + } + return count > 1; + } + + /* Deal with numbers in smallest possible units (satoshis) as much as possible. This greatly helps with accuracy + when rounding and whatnot. When we are storing numbers for only humans to see, store in whole coin units. */ + + var processPayments = function(){ + + var startPaymentProcess = Date.now(); + + var timeSpentRPC = 0; + var timeSpentRedis = 0; + + var startTimeRedis; + var startTimeRPC; + + var startRedisTimer = function(){ startTimeRedis = Date.now() }; + var endRedisTimer = function(){ timeSpentRedis += Date.now() - startTimeRedis }; + + var startRPCTimer = function(){ startTimeRPC = Date.now(); }; + var endRPCTimer = function(){ timeSpentRPC += Date.now() - startTimeRedis }; + + async.waterfall([ + /* + Step 1 - build workers and rounds objects from redis + * removes duplicate block submissions from redis + */ + function(callback){ + startRedisTimer(); + redisClient.multi([ + ['hgetall', coin + ':balances'], + ['smembers', coin + ':blocksPending'] + ]).exec(function(error, results){ + endRedisTimer(); + if (error){ + logger.error(logSystem, logComponent, 'Could not get blocks from redis ' + JSON.stringify(error)); + callback(true); + return; + } + // build workers object from :balances + var workers = {}; + for (var w in results[0]){ + workers[w] = {balance: coinsToSatoshies(parseFloat(results[0][w]))}; + } + // build rounds object from :blocksPending + var rounds = results[1].map(function(r){ + var details = r.split(':'); + return { + blockHash: details[0], + txHash: details[1], + height: details[2], + minedby: details[3], + time: details[4], + duplicate: false, + serialized: r + }; + }); + /* sort rounds by block hieght to pay in order */ + rounds.sort(function(a, b) { + return a.height - b.height; + }); + // find duplicate blocks by height + // this can happen when two or more solutions are submitted at the same block height + var duplicateFound = false; + for (var i = 0; i < rounds.length; i++) { + if (checkForDuplicateBlockHeight(rounds, rounds[i].height) === true) { + rounds[i].duplicate = true; + duplicateFound = true; + } + } + // handle duplicates if needed + if (duplicateFound) { + var dups = rounds.filter(function(round){ return round.duplicate; }); + logger.warning(logSystem, logComponent, 'Duplicate pending blocks found: ' + JSON.stringify(dups)); + // attempt to find the invalid duplicates + var rpcDupCheck = dups.map(function(r){ + return ['getblock', [r.blockHash]]; + }); + startRPCTimer(); + daemon.batchCmd(rpcDupCheck, function(error, blocks){ + endRPCTimer(); + if (error || !blocks) { + logger.error(logSystem, logComponent, 'Error with duplicate block check rpc call getblock ' + JSON.stringify(error)); + return; + } + // look for the invalid duplicate block + var validBlocks = {}; // hashtable for unique look up + var invalidBlocks = []; // array for redis work + blocks.forEach(function(block, i) { + if (block && block.result) { + // invalid duplicate submit blocks have negative confirmations + if (block.result.confirmations < 0) { + logger.warning(logSystem, logComponent, 'Remove invalid duplicate block ' + block.result.height + ' > ' + block.result.hash); + // move from blocksPending to blocksDuplicate... + invalidBlocks.push(['smove', coin + ':blocksPending', coin + ':blocksDuplicate', dups[i].serialized]); + } else { + // block must be valid, make sure it is unique + if (validBlocks.hasOwnProperty(dups[i].blockHash)) { + // not unique duplicate block + logger.warning(logSystem, logComponent, 'Remove non-unique duplicate block ' + block.result.height + ' > ' + block.result.hash); + // move from blocksPending to blocksDuplicate... + invalidBlocks.push(['smove', coin + ':blocksPending', coin + ':blocksDuplicate', dups[i].serialized]); + } else { + // keep unique valid block + validBlocks[dups[i].blockHash] = dups[i].serialized; + logger.debug(logSystem, logComponent, 'Keep valid duplicate block ' + block.result.height + ' > ' + block.result.hash); + } + } + } + }); + // filter out all duplicates to prevent double payments + rounds = rounds.filter(function(round){ return !round.duplicate; }); + // if we detected the invalid duplicates, move them + if (invalidBlocks.length > 0) { + // move invalid duplicate blocks in redis + startRedisTimer(); + redisClient.multi(invalidBlocks).exec(function(error, kicked){ + endRedisTimer(); + if (error) { + logger.error(logSystem, logComponent, 'Error could not move invalid duplicate blocks in redis ' + JSON.stringify(error)); + } + // continue payments normally + callback(null, workers, rounds); + }); + } else { + // notify pool owner that we are unable to find the invalid duplicate blocks, manual intervention required... + logger.error(logSystem, logComponent, 'Unable to detect invalid duplicate blocks, duplicate block payments on hold.'); + // continue payments normally + callback(null, workers, rounds); + } + }); + } else { + // no duplicates, continue payments normally + callback(null, workers, rounds); + } + }); + }, + + + /* + Step 2 - check if mined block coinbase tx are ready for payment + * adds block reward to rounds object + * adds block confirmations count to rounds object + */ + function(workers, rounds, callback){ + // get pending block tx details + var batchRPCcommand = rounds.map(function(r){ + return ['gettransaction', [r.txHash]]; + }); + // get account address (not implemented at this time) + batchRPCcommand.push(['getaccount', [poolOptions.address]]); + + startRPCTimer(); + daemon.batchCmd(batchRPCcommand, function(error, txDetails){ + endRPCTimer(); + if (error || !txDetails){ + logger.error(logSystem, logComponent, 'Check finished - daemon rpc error with batch gettransactions ' + JSON.stringify(error)); + callback(true); + return; + } + + var addressAccount = ""; + + // check for transaction errors and generated coins + txDetails.forEach(function(tx, i){ + if (i === txDetails.length - 1){ + if (tx.result && tx.result.toString().length > 0) { + addressAccount = tx.result.toString(); + } + return; + } + var round = rounds[i]; + // update confirmations for round + if (tx && tx.result) + round.confirmations = parseInt((tx.result.confirmations || 0)); + + // look for transaction errors + if (tx.error && tx.error.code === -5){ + logger.warning(logSystem, logComponent, 'Daemon reports invalid transaction: ' + round.txHash); + round.category = 'kicked'; + return; + } + else if (!tx.result.details || (tx.result.details && tx.result.details.length === 0)){ + logger.warning(logSystem, logComponent, 'Daemon reports no details for transaction: ' + round.txHash); + round.category = 'kicked'; + return; + } + else if (tx.error || !tx.result){ + logger.error(logSystem, logComponent, 'Odd error with gettransaction ' + round.txHash + ' ' + JSON.stringify(tx)); + return; + } + // get the coin base generation tx + var generationTx = tx.result.details.filter(function(tx){ + return tx.address === poolOptions.address; + })[0]; + if (!generationTx && tx.result.details.length === 1){ + generationTx = tx.result.details[0]; + } + if (!generationTx){ + logger.error(logSystem, logComponent, 'Missing output details to pool address for transaction ' + round.txHash); + return; + } + // get transaction category for round + round.category = generationTx.category; + // get reward for newly generated blocks + if (round.category === 'generate' || round.category === 'immature') { + round.reward = coinsRound(parseFloat(generationTx.amount || generationTx.value)); + } + }); + + var canDeleteShares = function(r){ + for (var i = 0; i < rounds.length; i++){ + var compareR = rounds[i]; + if ((compareR.height === r.height) + && (compareR.category !== 'kicked') + && (compareR.category !== 'orphan') + && (compareR.serialized !== r.serialized)){ + return false; + } + } + return true; + }; + + // only pay max blocks at a time + var payingBlocks = 0; + rounds = rounds.filter(function(r){ + switch (r.category) { + case 'orphan': + case 'kicked': + r.canDeleteShares = canDeleteShares(r); + case 'immature': + return true; + case 'generate': + payingBlocks++; + // if over maxBlocksPerPayment... + // change category to immature to prevent payment + // and to keep track of confirmations/immature balances + if (payingBlocks > maxBlocksPerPayment) + r.category = 'immature'; + return true; + default: + return false; + }; + }); + + // continue to next step in waterfall + callback(null, workers, rounds, addressAccount); + }); + }, + + + /* + Step 3 - lookup shares and calculate rewards + * pull pplnt times from redis + * pull shares from redis + * calculate rewards + * pplnt share reductions if needed + */ + function(workers, rounds, addressAccount, callback){ + // pplnt times lookup + var timeLookups = rounds.map(function(r){ + return ['hgetall', coin + ':shares:times' + r.height] + }); + startRedisTimer(); + redisClient.multi(timeLookups).exec(function(error, allWorkerTimes){ + endRedisTimer(); + if (error){ + callback('Check finished - redis error with multi get rounds time'); + return; + } + // shares lookup + var shareLookups = rounds.map(function(r){ + return ['hgetall', coin + ':shares:round' + r.height]; + }); + startRedisTimer(); + redisClient.multi(shareLookups).exec(function(error, allWorkerShares){ + endRedisTimer(); + if (error){ + callback('Check finished - redis error with multi get rounds share'); + return; + } + + // error detection + var err = null; + var performPayment = false; + + var notAddr = null; + if (requireShielding === true) { + notAddr = poolOptions.address; + } + + // calculate what the pool owes its miners + var feeSatoshi = coinsToSatoshies(fee); + var totalOwed = parseInt(0); + for (var i = 0; i < rounds.length; i++) { + // only pay generated blocks, not orphaned, kicked, immature + if (rounds[i].category == 'generate') { + totalOwed = totalOwed + coinsToSatoshies(rounds[i].reward) - feeSatoshi; + } + } + // also include balances owed + for (var w in workers) { + var worker = workers[w]; + totalOwed = totalOwed + (worker.balance||0); + } + // check if we have enough tAddress funds to begin payment processing + listUnspent(null, notAddr, minConfPayout, false, function (error, tBalance){ + if (error) { + logger.error(logSystem, logComponent, 'Error checking pool balance before processing payments.'); + return callback(true); + } else if (tBalance < totalOwed) { + logger.error(logSystem, logComponent, 'Insufficient funds ('+satoshisToCoins(tBalance) + ') to process payments (' + satoshisToCoins(totalOwed)+'); possibly waiting for txs.'); + performPayment = false; + } else if (tBalance > totalOwed) { + performPayment = true; + } + // just in case... + if (totalOwed <= 0) { + performPayment = false; + } + // if we can not perform payment + if (performPayment === false) { + // convert category generate to immature + rounds = rounds.filter(function(r){ + switch (r.category) { + case 'orphan': + case 'kicked': + case 'immature': + return true; + case 'generate': + r.category = 'immature'; + return true; + default: + return false; + }; + }); + } + + // handle rounds + rounds.forEach(function(round, i){ + var workerShares = allWorkerShares[i]; + if (!workerShares){ + err = true; + logger.error(logSystem, logComponent, 'No worker shares for round: ' + round.height + ' blockHash: ' + round.blockHash); + return; + } + var workerTimesWithPoolIds = allWorkerTimes[i]; + var workerTimes = {}; + var maxTime = 0; + if (pplntEnabled === true) { + for (var workerAddressWithPoolId in workerTimesWithPoolIds){ + var workerWithoutPoolId = workerAddressWithPoolId.split('.')[0]; + var workerTimeFloat = parseFloat(workerTimesWithPoolIds[workerAddressWithPoolId]); + if (maxTime < workerTimeFloat) { + maxTime = workerTimeFloat; + } + if (!(workerWithoutPoolId in workerTimes)) { + workerTimes[workerWithoutPoolId] = workerTimeFloat; + } else { + // add time from other instances with penalty + if (workerTimes[workerWithoutPoolId] < workerTimeFloat) { + workerTimes[workerWithoutPoolId] = workerTimes[workerWithoutPoolId] * 0.5 + workerTimeFloat; + } else { + workerTimes[workerWithoutPoolId] = workerTimes[workerWithoutPoolId] + workerTimeFloat * 0.5; + } + if (workerTimes[workerWithoutPoolId] > maxTime) { + workerTimes[workerWithoutPoolId] = maxTime; + } + } + } + } + switch (round.category){ + case 'kicked': + case 'orphan': + round.workerShares = workerShares; + break; + + /* calculate immature balances */ + case 'immature': + var feeSatoshi = coinsToSatoshies(fee); + var immature = coinsToSatoshies(round.reward); + var totalShares = parseFloat(0); + var sharesLost = parseFloat(0); + + // adjust block immature .. tx fees + immature = Math.round(immature - feeSatoshi); + + // total up shares for round + for (var workerAddress in workerShares){ + var worker = workers[workerAddress] = (workers[workerAddress] || {}); + var shares = parseFloat((workerShares[workerAddress] || 0)); + // if pplnt mode + if (pplntEnabled === true && maxTime > 0) { + var tshares = shares; + var lost = parseFloat(0); + var address = workerAddress.split('.')[0]; + if (workerTimes[address] != null && parseFloat(workerTimes[address]) > 0) { + var timePeriod = roundTo(parseFloat(workerTimes[address] || 1) / maxTime , 2); + if (timePeriod > 0 && timePeriod < pplntTimeQualify) { + var lost = shares - (shares * timePeriod); + sharesLost += lost; + shares = Math.max(shares - lost, 0); + } + } + } + worker.roundShares = shares; + totalShares += shares; + } + + //console.log('--IMMATURE DEBUG--------------'); + //console.log('performPayment: '+performPayment); + //console.log('blockHeight: '+round.height); + //console.log('blockReward: '+Math.round(immature)); + //console.log('blockConfirmations: '+round.confirmations); + + // calculate rewards for round + var totalAmount = 0; + for (var workerAddress in workerShares){ + var worker = workers[workerAddress] = (workers[workerAddress] || {}); + var percent = parseFloat(worker.roundShares) / totalShares; + // calculate workers immature for this round + var workerImmatureTotal = Math.round(immature * percent); + worker.immature = (worker.immature || 0) + workerImmatureTotal; + totalAmount += workerImmatureTotal; + } + + //console.log('----------------------------'); + break; + + /* calculate reward balances */ + case 'generate': + var feeSatoshi = coinsToSatoshies(fee); + var reward = coinsToSatoshies(round.reward); + var totalShares = parseFloat(0); + var sharesLost = parseFloat(0); + + // adjust block reward .. tx fees + reward = Math.round(reward - feeSatoshi); + + // total up shares for round + for (var workerAddress in workerShares){ + var worker = workers[workerAddress] = (workers[workerAddress] || {}); + var shares = parseFloat((workerShares[workerAddress] || 0)); + // if pplnt mode + if (pplntEnabled === true && maxTime > 0) { + var tshares = shares; + var lost = parseFloat(0); + var address = workerAddress.split('.')[0]; + if (workerTimes[address] != null && parseFloat(workerTimes[address]) > 0) { + var timePeriod = roundTo(parseFloat(workerTimes[address] || 1) / maxTime , 2); + if (timePeriod > 0 && timePeriod < pplntTimeQualify) { + var lost = shares - (shares * timePeriod); + sharesLost += lost; + shares = Math.max(shares - lost, 0); + logger.warning(logSystem, logComponent, 'PPLNT: Reduced shares for '+workerAddress+' round:' + round.height + ' maxTime:'+maxTime+'sec timePeriod:'+roundTo(timePeriod,6)+' shares:'+tshares+' lost:'+lost+' new:'+shares); + } + if (timePeriod > 1.0) { + err = true; + logger.error(logSystem, logComponent, 'Time share period is greater than 1.0 for '+workerAddress+' round:' + round.height + ' blockHash:' + round.blockHash); + return; + } + worker.timePeriod = timePeriod; + } + } + worker.roundShares = shares; + worker.totalShares = parseFloat(worker.totalShares || 0) + shares; + totalShares += shares; + } + + //console.log('--REWARD DEBUG--------------'); + //console.log('performPayment: '+performPayment); + //console.log('blockHeight: '+round.height); + //console.log('blockReward: ' + Math.round(reward)); + //console.log('blockConfirmations: '+round.confirmations); + + // calculate rewards for round + var totalAmount = 0; + for (var workerAddress in workerShares){ + var worker = workers[workerAddress] = (workers[workerAddress] || {}); + var percent = parseFloat(worker.roundShares) / totalShares; + if (percent > 1.0) { + err = true; + logger.error(logSystem, logComponent, 'Share percent is greater than 1.0 for '+workerAddress+' round:' + round.height + ' blockHash:' + round.blockHash); + return; + } + // calculate workers reward for this round + var workerRewardTotal = Math.round(reward * percent); + worker.reward = (worker.reward || 0) + workerRewardTotal; + totalAmount += workerRewardTotal; + } + + //console.log('----------------------------'); + break; + } + }); + + // if there was no errors + if (err === null) { + callback(null, workers, rounds, addressAccount); + } else { + // some error, stop waterfall + callback(true); + } + + }); // end funds check + });// end share lookup + }); // end time lookup + + }, + + + /* + Step 4 - Generate RPC commands to send payments + 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(workers, rounds, addressAccount, callback) { + + var tries = 0; + var trySend = function (withholdPercent) { + + var addressAmounts = {}; + var balanceAmounts = {}; + var shareAmounts = {}; + var timePeriods = {}; + var minerTotals = {}; + var totalSent = 0; + var totalShares = 0; + + // track attempts made, calls to trySend... + tries++; + + // total up miner's balances + for (var w in workers) { + var worker = workers[w]; + totalShares += (worker.totalShares || 0) + worker.balance = worker.balance || 0; + worker.reward = worker.reward || 0; + // get miner payout totals + var toSendSatoshis = Math.round((worker.balance + worker.reward) * (1 - withholdPercent)); + var address = worker.address = (worker.address || getProperAddress(w.split('.')[0])).trim(); + if (minerTotals[address] != null && minerTotals[address] > 0) { + minerTotals[address] += toSendSatoshis; + } else { + minerTotals[address] = toSendSatoshis; + } + } + // now process each workers balance, and pay the miner + for (var w in workers) { + var worker = workers[w]; + worker.balance = worker.balance || 0; + worker.reward = worker.reward || 0; + var toSendSatoshis = Math.round((worker.balance + worker.reward) * (1 - withholdPercent)); + var address = worker.address = (worker.address || getProperAddress(w.split('.')[0])).trim(); + // if miners total is enough, go ahead and add this worker balance + if (minerTotals[address] >= minPaymentSatoshis) { + totalSent += toSendSatoshis; + // send funds + worker.sent = satoshisToCoins(toSendSatoshis); + worker.balanceChange = Math.min(worker.balance, toSendSatoshis) * -1; + if (addressAmounts[address] != null && addressAmounts[address] > 0) { + addressAmounts[address] = coinsRound(addressAmounts[address] + worker.sent); + } else { + addressAmounts[address] = worker.sent; + } + } else { + // add to balance, not enough minerals + worker.sent = 0; + worker.balanceChange = Math.max(toSendSatoshis - worker.balance, 0); + // track balance changes + if (worker.balanceChange > 0) { + if (balanceAmounts[address] != null && balanceAmounts[address] > 0) { + balanceAmounts[address] = coinsRound(balanceAmounts[address] + satoshisToCoins(worker.balanceChange)); + } else { + balanceAmounts[address] = satoshisToCoins(worker.balanceChange); + } + } + } + // track share work + if (worker.totalShares > 0) { + if (shareAmounts[address] != null && shareAmounts[address] > 0) { + shareAmounts[address] += worker.totalShares; + } else { + shareAmounts[address] = worker.totalShares; + } + } + } + + // if no payouts...continue to next set of callbacks + if (Object.keys(addressAmounts).length === 0){ + callback(null, workers, rounds, []); + return; + } + + // do final rounding of payments per address + // this forces amounts to be valid (0.12345678) + for (var a in addressAmounts) { + addressAmounts[a] = coinsRound(addressAmounts[a]); + } + + // POINT OF NO RETURN! GOOD LUCK! + // WE ARE SENDING PAYMENT CMD TO DAEMON + + // perform the sendmany operation .. addressAccount + var rpccallTracking = 'sendmany "" '+JSON.stringify(addressAmounts); + //console.log(rpccallTracking); + + daemon.cmd('sendmany', ["", addressAmounts], function (result) { + // check for failed payments, there are many reasons + if (result.error && result.error.code === -6) { + // check if it is because we don't have enough funds + if (result.error.message && result.error.message.includes("insufficient funds")) { + // only try up to XX times (Max, 0.5%) + if (tries < 5) { + // we thought we had enough funds to send payments, but apparently not... + // try decreasing payments by a small percent to cover unexpected tx fees? + var higherPercent = withholdPercent + 0.001; // 0.1% + logger.warning(logSystem, logComponent, 'Insufficient funds (??) for payments ('+satoshisToCoins(totalSent)+'), decreasing rewards by ' + (higherPercent * 100).toFixed(1) + '% and retrying'); + trySend(higherPercent); + } else { + logger.warning(logSystem, logComponent, rpccallTracking); + logger.error(logSystem, logComponent, "Error sending payments, decreased rewards by too much!!!"); + callback(true); + } + } else { + // there was some fatal payment error? + logger.warning(logSystem, logComponent, rpccallTracking); + logger.error(logSystem, logComponent, 'Error sending payments ' + JSON.stringify(result.error)); + // payment failed, prevent updates to redis + callback(true); + } + return; + } + else if (result.error && result.error.code === -5) { + // invalid address specified in addressAmounts array + logger.warning(logSystem, logComponent, rpccallTracking); + logger.error(logSystem, logComponent, 'Error sending payments ' + JSON.stringify(result.error)); + // payment failed, prevent updates to redis + callback(true); + return; + } + else if (result.error && result.error.message != null) { + // invalid amount, others? + logger.warning(logSystem, logComponent, rpccallTracking); + logger.error(logSystem, logComponent, 'Error sending payments ' + JSON.stringify(result.error)); + // payment failed, prevent updates to redis + callback(true); + return; + } + else if (result.error) { + // unknown error + logger.error(logSystem, logComponent, 'Error sending payments ' + JSON.stringify(result.error)); + // payment failed, prevent updates to redis + callback(true); + return; + } + else { + + // make sure sendmany gives us back a txid + var txid = null; + if (result.response) { + txid = result.response; + } + if (txid != null) { + + // it worked, congrats on your pools payout ;) + logger.special(logSystem, logComponent, 'Sent ' + satoshisToCoins(totalSent) + + ' to ' + Object.keys(addressAmounts).length + ' miners; txid: '+txid); + + if (withholdPercent > 0) { + logger.warning(logSystem, logComponent, 'Had to withhold ' + (withholdPercent * 100) + + '% of reward from miners to cover transaction fees. ' + + 'Fund pool wallet with coins to prevent this from happening'); + } + + // save payments data to redis + var paymentBlocks = rounds.filter(function(r){ return r.category == 'generate'; }).map(function(r){ + return parseInt(r.height); + }); + + var paymentsUpdate = []; + var paymentsData = {time:Date.now(), txid:txid, shares:totalShares, paid:satoshisToCoins(totalSent), miners:Object.keys(addressAmounts).length, blocks: paymentBlocks, amounts: addressAmounts, balances: balanceAmounts, work:shareAmounts}; + paymentsUpdate.push(['zadd', logComponent + ':payments', Date.now(), JSON.stringify(paymentsData)]); + + callback(null, workers, rounds, paymentsUpdate); + + } else { + + clearInterval(paymentInterval); + + logger.error(logSystem, logComponent, 'Error RPC sendmany did not return txid ' + + JSON.stringify(result) + 'Disabling payment processing to prevent possible double-payouts.'); + + callback(true); + return; + } + } + }, true, true); + }; + + // attempt to send any owed payments + trySend(0); + }, + + + /* + Step 5 - Final redis commands + */ + function(workers, rounds, paymentsUpdate, callback){ + + var totalPaid = parseFloat(0); + + var immatureUpdateCommands = []; + var balanceUpdateCommands = []; + var workerPayoutsCommand = []; + + // update worker paid/balance stats + for (var w in workers) { + var worker = workers[w]; + // update balances + if ((worker.balanceChange || 0) !== 0){ + balanceUpdateCommands.push([ + 'hincrbyfloat', + coin + ':balances', + w, + satoshisToCoins(worker.balanceChange) + ]); + } + // update payouts + if ((worker.sent || 0) > 0){ + workerPayoutsCommand.push(['hincrbyfloat', coin + ':payouts', w, coinsRound(worker.sent)]); + totalPaid = coinsRound(totalPaid + worker.sent); + } + // update immature balances + if ((worker.immature || 0) > 0) { + immatureUpdateCommands.push(['hset', coin + ':immature', w, worker.immature]); + } else { + immatureUpdateCommands.push(['hset', coin + ':immature', w, 0]); + } + } + + var movePendingCommands = []; + var roundsToDelete = []; + var orphanMergeCommands = []; + + var confirmsUpdate = []; + var confirmsToDelete = []; + + var moveSharesToCurrent = function(r){ + var workerShares = r.workerShares; + if (workerShares != null) { + logger.warning(logSystem, logComponent, 'Moving shares from orphaned block '+r.height+' to current round.'); + Object.keys(workerShares).forEach(function(worker){ + orphanMergeCommands.push(['hincrby', coin + ':shares:roundCurrent', worker, workerShares[worker]]); + }); + } + }; + + rounds.forEach(function(r){ + switch(r.category){ + case 'kicked': + case 'orphan': + confirmsToDelete.push(['hdel', coin + ':blocksPendingConfirms', r.blockHash]); + movePendingCommands.push(['smove', coin + ':blocksPending', coin + ':blocksKicked', r.serialized]); + if (r.canDeleteShares){ + moveSharesToCurrent(r); + roundsToDelete.push(coin + ':shares:round' + r.height); + roundsToDelete.push(coin + ':shares:times' + r.height); + } + return; + case 'immature': + confirmsUpdate.push(['hset', coin + ':blocksPendingConfirms', r.blockHash, (r.confirmations || 0)]); + return; + case 'generate': + confirmsToDelete.push(['hdel', coin + ':blocksPendingConfirms', r.blockHash]); + movePendingCommands.push(['smove', coin + ':blocksPending', coin + ':blocksConfirmed', r.serialized]); + roundsToDelete.push(coin + ':shares:round' + r.height); + roundsToDelete.push(coin + ':shares:times' + r.height); + return; + } + }); + + var finalRedisCommands = []; + + if (movePendingCommands.length > 0) + finalRedisCommands = finalRedisCommands.concat(movePendingCommands); + + if (orphanMergeCommands.length > 0) + finalRedisCommands = finalRedisCommands.concat(orphanMergeCommands); + + if (immatureUpdateCommands.length > 0) + finalRedisCommands = finalRedisCommands.concat(immatureUpdateCommands); + + if (balanceUpdateCommands.length > 0) + finalRedisCommands = finalRedisCommands.concat(balanceUpdateCommands); + + if (workerPayoutsCommand.length > 0) + finalRedisCommands = finalRedisCommands.concat(workerPayoutsCommand); + + if (roundsToDelete.length > 0) + finalRedisCommands.push(['del'].concat(roundsToDelete)); + + if (confirmsUpdate.length > 0) + finalRedisCommands = finalRedisCommands.concat(confirmsUpdate); + + if (confirmsToDelete.length > 0) + finalRedisCommands = finalRedisCommands.concat(confirmsToDelete); + + if (paymentsUpdate.length > 0) + finalRedisCommands = finalRedisCommands.concat(paymentsUpdate); + + if (totalPaid !== 0) + finalRedisCommands.push(['hincrbyfloat', coin + ':stats', 'totalPaid', totalPaid]); + + if (finalRedisCommands.length === 0){ + callback(); + return; + } + + startRedisTimer(); + redisClient.multi(finalRedisCommands).exec(function(error, results){ + endRedisTimer(); + if (error) { + clearInterval(paymentInterval); + + logger.error(logSystem, logComponent, + 'Payments sent but could not update redis. ' + JSON.stringify(error) + + ' Disabling payment processing to prevent possible double-payouts. The redis commands in ' + + coin + '_finalRedisCommands.txt must be ran manually'); + + fs.writeFile(coin + '_finalRedisCommands.txt', JSON.stringify(finalRedisCommands), function(err){ + logger.error('Could not write finalRedisCommands.txt, you are fucked.'); + }); + } + callback(); + }); + } + + ], function(){ + + var paymentProcessTime = Date.now() - startPaymentProcess; + logger.debug(logSystem, logComponent, 'Finished interval - time spent: ' + + paymentProcessTime + 'ms total, ' + timeSpentRedis + 'ms redis, ' + + timeSpentRPC + 'ms daemon RPC'); + + }); + }; + + + var getProperAddress = function(address){ + if (address.length >= 40){ + logger.warning(logSystem, logComponent, 'Invalid address '+address+', convert to address '+(poolOptions.invalidAddress || poolOptions.address)); + return (poolOptions.invalidAddress || poolOptions.address); + } + if (address.length <= 30) { + logger.warning(logSystem, logComponent, 'Invalid address '+address+', convert to address '+(poolOptions.invalidAddress || poolOptions.address)); + return (poolOptions.invalidAddress || poolOptions.address); + } + return address; + }; + +} diff --git a/libs/poolWorker.js b/libs/poolWorker.js new file mode 100644 index 0000000..1f061f0 --- /dev/null +++ b/libs/poolWorker.js @@ -0,0 +1,329 @@ +var Stratum = require('stratum-pool'); +var redis = require('redis'); +var net = require('net'); + +var MposCompatibility = require('./mposCompatibility.js'); +var ShareProcessor = require('./shareProcessor.js'); + +module.exports = function(logger){ + + var _this = this; + + var poolConfigs = JSON.parse(process.env.pools); + var portalConfig = JSON.parse(process.env.portalConfig); + + var forkId = process.env.forkId; + + var pools = {}; + + var proxySwitch = {}; + + var redisClient = redis.createClient(portalConfig.redis.port, portalConfig.redis.host); + if (portalConfig.redis.password) { + redisClient.auth(portalConfig.redis.password); + } + //Handle messages from master process sent via IPC + process.on('message', function(message) { + switch(message.type){ + + case 'banIP': + for (var p in pools){ + if (pools[p].stratumServer) + pools[p].stratumServer.addBannedIP(message.ip); + } + break; + + case 'blocknotify': + + var messageCoin = message.coin.toLowerCase(); + var poolTarget = Object.keys(pools).filter(function(p){ + return p.toLowerCase() === messageCoin; + })[0]; + + if (poolTarget) + pools[poolTarget].processBlockNotify(message.hash, 'blocknotify script'); + + break; + + // IPC message for pool switching + case 'coinswitch': + var logSystem = 'Proxy'; + var logComponent = 'Switch'; + var logSubCat = 'Thread ' + (parseInt(forkId) + 1); + + var switchName = message.switchName; + + var newCoin = message.coin; + + var algo = poolConfigs[newCoin].coin.algorithm; + + var newPool = pools[newCoin]; + var oldCoin = proxySwitch[switchName].currentPool; + var oldPool = pools[oldCoin]; + var proxyPorts = Object.keys(proxySwitch[switchName].ports); + + if (newCoin == oldCoin) { + logger.debug(logSystem, logComponent, logSubCat, 'Switch message would have no effect - ignoring ' + newCoin); + break; + } + + logger.debug(logSystem, logComponent, logSubCat, 'Proxy message for ' + algo + ' from ' + oldCoin + ' to ' + newCoin); + + if (newPool) { + oldPool.relinquishMiners( + function (miner, cback) { + // relinquish miners that are attached to one of the "Auto-switch" ports and leave the others there. + cback(proxyPorts.indexOf(miner.client.socket.localPort.toString()) !== -1) + }, + function (clients) { + newPool.attachMiners(clients); + } + ); + proxySwitch[switchName].currentPool = newCoin; + + redisClient.hset('proxyState', algo, newCoin, function(error, obj) { + if (error) { + logger.error(logSystem, logComponent, logSubCat, 'Redis error writing proxy config: ' + JSON.stringify(err)) + } + else { + logger.debug(logSystem, logComponent, logSubCat, 'Last proxy state saved to redis for ' + algo); + } + }); + + } + break; + } + }); + + + Object.keys(poolConfigs).forEach(function(coin) { + + var poolOptions = poolConfigs[coin]; + + var logSystem = 'Pool'; + var logComponent = coin; + var logSubCat = 'Thread ' + (parseInt(forkId) + 1); + + var handlers = { + auth: function(){}, + share: function(){}, + diff: function(){} + }; + + //Functions required for MPOS compatibility + if (poolOptions.mposMode && poolOptions.mposMode.enabled){ + var mposCompat = new MposCompatibility(logger, poolOptions); + + handlers.auth = function(port, workerName, password, authCallback){ + mposCompat.handleAuth(workerName, password, authCallback); + }; + + handlers.share = function(isValidShare, isValidBlock, data){ + mposCompat.handleShare(isValidShare, isValidBlock, data); + }; + + handlers.diff = function(workerName, diff){ + mposCompat.handleDifficultyUpdate(workerName, diff); + } + } + + //Functions required for internal payment processing + else{ + + var shareProcessor = new ShareProcessor(logger, poolOptions); + + handlers.auth = function(port, workerName, password, authCallback){ + if (poolOptions.validateWorkerUsername !== true) + authCallback(true); + else { + pool.daemon.cmd('validateaddress', [String(workerName).split(".")[0]], function (results) { + var isValid = results.filter(function (r) { + return r.response.isvalid + }).length > 0; + authCallback(isValid); + }); + } + }; + + handlers.share = function(isValidShare, isValidBlock, data){ + shareProcessor.handleShare(isValidShare, isValidBlock, data); + }; + } + + var authorizeFN = function (ip, port, workerName, password, callback) { + handlers.auth(port, workerName, password, function(authorized){ + + var authString = authorized ? 'Authorized' : 'Unauthorized '; + + logger.debug(logSystem, logComponent, logSubCat, authString + ' ' + workerName + ':' + password + ' [' + ip + ']'); + callback({ + error: null, + authorized: authorized, + disconnect: false + }); + }); + }; + + + var pool = Stratum.createPool(poolOptions, authorizeFN, logger); + pool.on('share', function(isValidShare, isValidBlock, data){ + + var shareData = JSON.stringify(data); + + if (data.blockHash && !isValidBlock) + logger.debug(logSystem, logComponent, logSubCat, 'We thought a block was found but it was rejected by the daemon, share data: ' + shareData); + + else if (isValidBlock) + logger.debug(logSystem, logComponent, logSubCat, 'Block found: ' + data.blockHash + ' by ' + data.worker); + + if (isValidShare) { + if(data.shareDiff > 1000000000) { + logger.debug(logSystem, logComponent, logSubCat, 'Share was found with diff higher than 1.000.000.000!'); + } else if(data.shareDiff > 1000000) { + logger.debug(logSystem, logComponent, logSubCat, 'Share was found with diff higher than 1.000.000!'); + } + //logger.debug(logSystem, logComponent, logSubCat, 'Share accepted at diff ' + data.difficulty + '/' + data.shareDiff + ' by ' + data.worker + ' [' + data.ip + ']' ); + } else if (!isValidShare) { + logger.debug(logSystem, logComponent, logSubCat, 'Share rejected: ' + shareData); + } + + // handle the share + handlers.share(isValidShare, isValidBlock, data); + + // send to master for pplnt time tracking + process.send({type: 'shareTrack', thread:(parseInt(forkId)+1), coin:poolOptions.coin.name, isValidShare:isValidShare, isValidBlock:isValidBlock, data:data}); + + }).on('difficultyUpdate', function(workerName, diff){ + logger.debug(logSystem, logComponent, logSubCat, 'Difficulty update to diff ' + diff + ' workerName=' + JSON.stringify(workerName)); + handlers.diff(workerName, diff); + }).on('log', function(severity, text) { + logger[severity](logSystem, logComponent, logSubCat, text); + }).on('banIP', function(ip, worker){ + process.send({type: 'banIP', ip: ip}); + }).on('started', function(){ + _this.setDifficultyForProxyPort(pool, poolOptions.coin.name, poolOptions.coin.algorithm); + }); + + pool.start(); + pools[poolOptions.coin.name] = pool; + }); + + + if (portalConfig.switching) { + + var logSystem = 'Switching'; + var logComponent = 'Setup'; + var logSubCat = 'Thread ' + (parseInt(forkId) + 1); + + var proxyState = {}; + + // + // Load proxy state for each algorithm from redis which allows NOMP to resume operation + // on the last pool it was using when reloaded or restarted + // + logger.debug(logSystem, logComponent, logSubCat, 'Loading last proxy state from redis'); + + + + /*redisClient.on('error', function(err){ + logger.debug(logSystem, logComponent, logSubCat, 'Pool configuration failed: ' + err); + });*/ + + redisClient.hgetall("proxyState", function(error, obj) { + if (!error && obj) { + proxyState = obj; + logger.debug(logSystem, logComponent, logSubCat, 'Last proxy state loaded from redis'); + } + + // + // Setup proxySwitch object to control proxy operations from configuration and any restored + // state. Each algorithm has a listening port, current coin name, and an active pool to + // which traffic is directed when activated in the config. + // + // In addition, the proxy config also takes diff and varDiff parmeters the override the + // defaults for the standard config of the coin. + // + Object.keys(portalConfig.switching).forEach(function(switchName) { + + var algorithm = portalConfig.switching[switchName].algorithm; + + if (!portalConfig.switching[switchName].enabled) return; + + + var initalPool = proxyState.hasOwnProperty(algorithm) ? proxyState[algorithm] : _this.getFirstPoolForAlgorithm(algorithm); + proxySwitch[switchName] = { + algorithm: algorithm, + ports: portalConfig.switching[switchName].ports, + currentPool: initalPool, + servers: [] + }; + + + Object.keys(proxySwitch[switchName].ports).forEach(function(port){ + var f = net.createServer(function(socket) { + var currentPool = proxySwitch[switchName].currentPool; + + logger.debug(logSystem, 'Connect', logSubCat, 'Connection to ' + + switchName + ' from ' + + socket.remoteAddress + ' on ' + + port + ' routing to ' + currentPool); + + if (pools[currentPool]) + pools[currentPool].getStratumServer().handleNewClient(socket); + else + pools[initalPool].getStratumServer().handleNewClient(socket); + + }).listen(parseInt(port), function() { + logger.debug(logSystem, logComponent, logSubCat, 'Switching "' + switchName + + '" listening for ' + algorithm + + ' on port ' + port + + ' into ' + proxySwitch[switchName].currentPool); + }); + proxySwitch[switchName].servers.push(f); + }); + + }); + }); + } + + this.getFirstPoolForAlgorithm = function(algorithm) { + var foundCoin = ""; + Object.keys(poolConfigs).forEach(function(coinName) { + if (poolConfigs[coinName].coin.algorithm == algorithm) { + if (foundCoin === "") + foundCoin = coinName; + } + }); + return foundCoin; + }; + + // + // Called when stratum pool emits its 'started' event to copy the initial diff and vardiff + // configuation for any proxy switching ports configured into the stratum pool object. + // + this.setDifficultyForProxyPort = function(pool, coin, algo) { + + logger.debug(logSystem, logComponent, algo, 'Setting proxy difficulties after pool start'); + + Object.keys(portalConfig.switching).forEach(function(switchName) { + if (!portalConfig.switching[switchName].enabled) return; + + var switchAlgo = portalConfig.switching[switchName].algorithm; + if (pool.options.coin.algorithm !== switchAlgo) return; + + // we know the switch configuration matches the pool's algo, so setup the diff and + // vardiff for each of the switch's ports + for (var port in portalConfig.switching[switchName].ports) { + + if (portalConfig.switching[switchName].ports[port].varDiff) + pool.setVarDiff(port, portalConfig.switching[switchName].ports[port].varDiff); + + if (portalConfig.switching[switchName].ports[port].diff){ + if (!pool.options.ports.hasOwnProperty(port)) + pool.options.ports[port] = {}; + pool.options.ports[port].diff = portalConfig.switching[switchName].ports[port].diff; + } + } + }); + }; +}; diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js new file mode 100644 index 0000000..990314e --- /dev/null +++ b/libs/profitSwitch.js @@ -0,0 +1,666 @@ +var async = require('async'); +var net = require('net'); +var bignum = require('bignum'); +var algos = require('stratum-pool/lib/algoProperties.js'); +var util = require('stratum-pool/lib/util.js'); + +var Cryptsy = require('./apiCryptsy.js'); +var Poloniex = require('./apiPoloniex.js'); +var Mintpal = require('./apiMintpal.js'); +var Bittrex = require('./apiBittrex.js'); +var Stratum = require('stratum-pool'); + +module.exports = function(logger){ + + var _this = this; + + var portalConfig = JSON.parse(process.env.portalConfig); + var poolConfigs = JSON.parse(process.env.pools); + + var logSystem = 'Profit'; + + // + // build status tracker for collecting coin market information + // + var profitStatus = {}; + var symbolToAlgorithmMap = {}; + Object.keys(poolConfigs).forEach(function(coin){ + + var poolConfig = poolConfigs[coin]; + var algo = poolConfig.coin.algorithm; + + if (!profitStatus.hasOwnProperty(algo)) { + profitStatus[algo] = {}; + } + var coinStatus = { + name: poolConfig.coin.name, + symbol: poolConfig.coin.symbol, + difficulty: 0, + reward: 0, + exchangeInfo: {} + }; + profitStatus[algo][poolConfig.coin.symbol] = coinStatus; + symbolToAlgorithmMap[poolConfig.coin.symbol] = algo; + }); + + + // + // ensure we have something to switch + // + Object.keys(profitStatus).forEach(function(algo){ + if (Object.keys(profitStatus[algo]).length <= 1) { + delete profitStatus[algo]; + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + if (symbolToAlgorithmMap[symbol] === algo) + delete symbolToAlgorithmMap[symbol]; + }); + } + }); + if (Object.keys(profitStatus).length == 0){ + logger.debug(logSystem, 'Config', 'No alternative coins to switch to in current config, switching disabled.'); + return; + } + + + // + // setup APIs + // + var poloApi = new Poloniex( + // 'API_KEY', + // 'API_SECRET' + ); + var cryptsyApi = new Cryptsy( + // 'API_KEY', + // 'API_SECRET' + ); + var mintpalApi = new Mintpal( + // 'API_KEY', + // 'API_SECRET' + ); + + var bittrexApi = new Bittrex( + // 'API_KEY', + // 'API_SECRET' + ); + + // + // market data collection from Poloniex + // + this.getProfitDataPoloniex = function(callback){ + async.series([ + function(taskCallback){ + poloApi.getTicker(function(err, data){ + if (err){ + taskCallback(err); + return; + } + + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; + if (!exchangeInfo.hasOwnProperty('Poloniex')) + exchangeInfo['Poloniex'] = {}; + var marketData = exchangeInfo['Poloniex']; + + if (data.hasOwnProperty('BTC_' + symbol)) { + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; + + var btcData = data['BTC_' + symbol]; + marketData['BTC'].ask = new Number(btcData.lowestAsk); + marketData['BTC'].bid = new Number(btcData.highestBid); + marketData['BTC'].last = new Number(btcData.last); + marketData['BTC'].baseVolume = new Number(btcData.baseVolume); + marketData['BTC'].quoteVolume = new Number(btcData.quoteVolume); + } + if (data.hasOwnProperty('LTC_' + symbol)) { + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; + + var ltcData = data['LTC_' + symbol]; + marketData['LTC'].ask = new Number(ltcData.lowestAsk); + marketData['LTC'].bid = new Number(ltcData.highestBid); + marketData['LTC'].last = new Number(ltcData.last); + marketData['LTC'].baseVolume = new Number(ltcData.baseVolume); + marketData['LTC'].quoteVolume = new Number(ltcData.quoteVolume); + } + // save LTC to BTC exchange rate + if (marketData.hasOwnProperty('LTC') && data.hasOwnProperty('BTC_LTC')) { + var btcLtc = data['BTC_LTC']; + marketData['LTC'].ltcToBtc = new Number(btcLtc.highestBid); + } + }); + + taskCallback(); + }); + }, + function(taskCallback){ + var depthTasks = []; + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + var marketData = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo['Poloniex']; + if (marketData.hasOwnProperty('BTC') && marketData['BTC'].bid > 0){ + depthTasks.push(function(callback){ + _this.getMarketDepthFromPoloniex('BTC', symbol, marketData['BTC'].bid, callback) + }); + } + if (marketData.hasOwnProperty('LTC') && marketData['LTC'].bid > 0){ + depthTasks.push(function(callback){ + _this.getMarketDepthFromPoloniex('LTC', symbol, marketData['LTC'].bid, callback) + }); + } + }); + + if (!depthTasks.length){ + taskCallback(); + return; + } + async.series(depthTasks, function(err){ + if (err){ + taskCallback(err); + return; + } + taskCallback(); + }); + } + ], function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + + }; + this.getMarketDepthFromPoloniex = function(symbolA, symbolB, coinPrice, callback){ + poloApi.getOrderBook(symbolA, symbolB, function(err, data){ + if (err){ + callback(err); + return; + } + var depth = new Number(0); + var totalQty = new Number(0); + if (data.hasOwnProperty('bids')){ + data['bids'].forEach(function(order){ + var price = new Number(order[0]); + var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); + var qty = new Number(order[1]); + // only measure the depth down to configured depth + if (price >= limit){ + depth += (qty * price); + totalQty += qty; + } + }); + } + + var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Poloniex']; + marketData[symbolA].depth = depth; + if (totalQty > 0) + marketData[symbolA].weightedBid = new Number(depth / totalQty); + callback(); + }); + }; + + + this.getProfitDataCryptsy = function(callback){ + async.series([ + function(taskCallback){ + cryptsyApi.getTicker(function(err, data){ + if (err || data.success != 1){ + taskCallback(err); + return; + } + + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; + if (!exchangeInfo.hasOwnProperty('Cryptsy')) + exchangeInfo['Cryptsy'] = {}; + + var marketData = exchangeInfo['Cryptsy']; + var results = data.return.markets; + + if (results && results.hasOwnProperty(symbol + '/BTC')) { + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; + + var btcData = results[symbol + '/BTC']; + marketData['BTC'].last = new Number(btcData.lasttradeprice); + marketData['BTC'].baseVolume = new Number(marketData['BTC'].last / btcData.volume); + marketData['BTC'].quoteVolume = new Number(btcData.volume); + if (btcData.sellorders != null) + marketData['BTC'].ask = new Number(btcData.sellorders[0].price); + if (btcData.buyorders != null) { + marketData['BTC'].bid = new Number(btcData.buyorders[0].price); + var limit = new Number(marketData['BTC'].bid * portalConfig.profitSwitch.depth); + var depth = new Number(0); + var totalQty = new Number(0); + btcData['buyorders'].forEach(function(order){ + var price = new Number(order.price); + var qty = new Number(order.quantity); + if (price >= limit){ + depth += (qty * price); + totalQty += qty; + } + }); + marketData['BTC'].depth = depth; + if (totalQty > 0) + marketData['BTC'].weightedBid = new Number(depth / totalQty); + } + } + + if (results && results.hasOwnProperty(symbol + '/LTC')) { + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; + + var ltcData = results[symbol + '/LTC']; + marketData['LTC'].last = new Number(ltcData.lasttradeprice); + marketData['LTC'].baseVolume = new Number(marketData['LTC'].last / ltcData.volume); + marketData['LTC'].quoteVolume = new Number(ltcData.volume); + if (ltcData.sellorders != null) + marketData['LTC'].ask = new Number(ltcData.sellorders[0].price); + if (ltcData.buyorders != null) { + marketData['LTC'].bid = new Number(ltcData.buyorders[0].price); + var limit = new Number(marketData['LTC'].bid * portalConfig.profitSwitch.depth); + var depth = new Number(0); + var totalQty = new Number(0); + ltcData['buyorders'].forEach(function(order){ + var price = new Number(order.price); + var qty = new Number(order.quantity); + if (price >= limit){ + depth += (qty * price); + totalQty += qty; + } + }); + marketData['LTC'].depth = depth; + if (totalQty > 0) + marketData['LTC'].weightedBid = new Number(depth / totalQty); + } + } + }); + taskCallback(); + }); + } + ], function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + + }; + + + this.getProfitDataMintpal = function(callback){ + async.series([ + function(taskCallback){ + mintpalApi.getTicker(function(err, response){ + if (err || !response.data){ + taskCallback(err); + return; + } + + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + response.data.forEach(function(market){ + var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; + if (!exchangeInfo.hasOwnProperty('Mintpal')) + exchangeInfo['Mintpal'] = {}; + + var marketData = exchangeInfo['Mintpal']; + + if (market.exchange == 'BTC' && market.code == symbol) { + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; + + marketData['BTC'].last = new Number(market.last_price); + marketData['BTC'].baseVolume = new Number(market['24hvol']); + marketData['BTC'].quoteVolume = new Number(market['24hvol'] / market.last_price); + marketData['BTC'].ask = new Number(market.top_ask); + marketData['BTC'].bid = new Number(market.top_bid); + } + + if (market.exchange == 'LTC' && market.code == symbol) { + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; + + marketData['LTC'].last = new Number(market.last_price); + marketData['LTC'].baseVolume = new Number(market['24hvol']); + marketData['LTC'].quoteVolume = new Number(market['24hvol'] / market.last_price); + marketData['LTC'].ask = new Number(market.top_ask); + marketData['LTC'].bid = new Number(market.top_bid); + } + + }); + }); + taskCallback(); + }); + }, + function(taskCallback){ + var depthTasks = []; + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + var marketData = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo['Mintpal']; + if (marketData.hasOwnProperty('BTC') && marketData['BTC'].bid > 0){ + depthTasks.push(function(callback){ + _this.getMarketDepthFromMintpal('BTC', symbol, marketData['BTC'].bid, callback) + }); + } + if (marketData.hasOwnProperty('LTC') && marketData['LTC'].bid > 0){ + depthTasks.push(function(callback){ + _this.getMarketDepthFromMintpal('LTC', symbol, marketData['LTC'].bid, callback) + }); + } + }); + + if (!depthTasks.length){ + taskCallback(); + return; + } + async.series(depthTasks, function(err){ + if (err){ + taskCallback(err); + return; + } + taskCallback(); + }); + } + ], function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + }; + this.getMarketDepthFromMintpal = function(symbolA, symbolB, coinPrice, callback){ + mintpalApi.getBuyOrderBook(symbolA, symbolB, function(err, response){ + if (err){ + callback(err); + return; + } + var depth = new Number(0); + if (response.hasOwnProperty('data')){ + var totalQty = new Number(0); + response['data'].forEach(function(order){ + var price = new Number(order.price); + var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); + var qty = new Number(order.amount); + // only measure the depth down to configured depth + if (price >= limit){ + depth += (qty * price); + totalQty += qty; + } + }); + } + + var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Mintpal']; + marketData[symbolA].depth = depth; + if (totalQty > 0) + marketData[symbolA].weightedBid = new Number(depth / totalQty); + callback(); + }); + }; + + + this.getProfitDataBittrex = function(callback){ + async.series([ + function(taskCallback){ + bittrexApi.getTicker(function(err, response){ + if (err || !response.result){ + taskCallback(err); + return; + } + + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + response.result.forEach(function(market){ + var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; + if (!exchangeInfo.hasOwnProperty('Bittrex')) + exchangeInfo['Bittrex'] = {}; + + var marketData = exchangeInfo['Bittrex']; + var marketPair = market.MarketName.match(/([\w]+)-([\w-_]+)/) + market.exchange = marketPair[1] + market.code = marketPair[2] + if (market.exchange == 'BTC' && market.code == symbol) { + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; + + marketData['BTC'].last = new Number(market.Last); + marketData['BTC'].baseVolume = new Number(market.BaseVolume); + marketData['BTC'].quoteVolume = new Number(market.BaseVolume / market.Last); + marketData['BTC'].ask = new Number(market.Ask); + marketData['BTC'].bid = new Number(market.Bid); + } + + if (market.exchange == 'LTC' && market.code == symbol) { + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; + + marketData['LTC'].last = new Number(market.Last); + marketData['LTC'].baseVolume = new Number(market.BaseVolume); + marketData['LTC'].quoteVolume = new Number(market.BaseVolume / market.Last); + marketData['LTC'].ask = new Number(market.Ask); + marketData['LTC'].bid = new Number(market.Bid); + } + + }); + }); + taskCallback(); + }); + }, + function(taskCallback){ + var depthTasks = []; + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + var marketData = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo['Bittrex']; + if (marketData.hasOwnProperty('BTC') && marketData['BTC'].bid > 0){ + depthTasks.push(function(callback){ + _this.getMarketDepthFromBittrex('BTC', symbol, marketData['BTC'].bid, callback) + }); + } + if (marketData.hasOwnProperty('LTC') && marketData['LTC'].bid > 0){ + depthTasks.push(function(callback){ + _this.getMarketDepthFromBittrex('LTC', symbol, marketData['LTC'].bid, callback) + }); + } + }); + + if (!depthTasks.length){ + taskCallback(); + return; + } + async.series(depthTasks, function(err){ + if (err){ + taskCallback(err); + return; + } + taskCallback(); + }); + } + ], function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + }; + this.getMarketDepthFromBittrex = function(symbolA, symbolB, coinPrice, callback){ + bittrexApi.getOrderBook(symbolA, symbolB, function(err, response){ + if (err){ + callback(err); + return; + } + var depth = new Number(0); + if (response.hasOwnProperty('result')){ + var totalQty = new Number(0); + response['result'].forEach(function(order){ + var price = new Number(order.Rate); + var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); + var qty = new Number(order.Quantity); + // only measure the depth down to configured depth + if (price >= limit){ + depth += (qty * price); + totalQty += qty; + } + }); + } + + var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Bittrex']; + marketData[symbolA].depth = depth; + if (totalQty > 0) + marketData[symbolA].weightedBid = new Number(depth / totalQty); + callback(); + }); + }; + + + this.getCoindDaemonInfo = function(callback){ + var daemonTasks = []; + Object.keys(profitStatus).forEach(function(algo){ + Object.keys(profitStatus[algo]).forEach(function(symbol){ + var coinName = profitStatus[algo][symbol].name; + var poolConfig = poolConfigs[coinName]; + var daemonConfig = poolConfig.paymentProcessing.daemon; + daemonTasks.push(function(callback){ + _this.getDaemonInfoForCoin(symbol, daemonConfig, callback) + }); + }); + }); + + if (daemonTasks.length == 0){ + callback(); + return; + } + async.series(daemonTasks, function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + }; + this.getDaemonInfoForCoin = function(symbol, cfg, callback){ + var daemon = new Stratum.daemon.interface([cfg], function(severity, message){ + logger[severity](logSystem, symbol, message); + callback(null); // fail gracefully for each coin + }); + + daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], function(result) { + if (result[0].error != null) { + logger.error(logSystem, symbol, 'Error while reading daemon info: ' + JSON.stringify(result[0])); + callback(null); // fail gracefully for each coin + return; + } + var coinStatus = profitStatus[symbolToAlgorithmMap[symbol]][symbol]; + var response = result[0].response; + + // some shitcoins dont provide target, only bits, so we need to deal with both + var target = response.target ? bignum(response.target, 16) : util.bignumFromBitsHex(response.bits); + coinStatus.difficulty = parseFloat((diff1 / target.toNumber()).toFixed(9)); + logger.debug(logSystem, symbol, 'difficulty is ' + coinStatus.difficulty); + + coinStatus.reward = response.coinbasevalue / 100000000; + callback(null); + }); + }; + + + this.getMiningRate = function(callback){ + var daemonTasks = []; + Object.keys(profitStatus).forEach(function(algo){ + Object.keys(profitStatus[algo]).forEach(function(symbol){ + var coinStatus = profitStatus[symbolToAlgorithmMap[symbol]][symbol]; + coinStatus.blocksPerMhPerHour = 86400 / ((coinStatus.difficulty * Math.pow(2,32)) / (1 * 1000 * 1000)); + coinStatus.coinsPerMhPerHour = coinStatus.reward * coinStatus.blocksPerMhPerHour; + }); + }); + callback(null); + }; + + + this.switchToMostProfitableCoins = function() { + Object.keys(profitStatus).forEach(function(algo) { + var algoStatus = profitStatus[algo]; + + var bestExchange; + var bestCoin; + var bestBtcPerMhPerHour = 0; + + Object.keys(profitStatus[algo]).forEach(function(symbol) { + var coinStatus = profitStatus[algo][symbol]; + + Object.keys(coinStatus.exchangeInfo).forEach(function(exchange){ + var exchangeData = coinStatus.exchangeInfo[exchange]; + if (exchangeData.hasOwnProperty('BTC') && exchangeData['BTC'].hasOwnProperty('weightedBid')){ + var btcPerMhPerHour = exchangeData['BTC'].weightedBid * coinStatus.coinsPerMhPerHour; + if (btcPerMhPerHour > bestBtcPerMhPerHour){ + bestBtcPerMhPerHour = btcPerMhPerHour; + bestExchange = exchange; + bestCoin = profitStatus[algo][symbol].name; + } + coinStatus.btcPerMhPerHour = btcPerMhPerHour; + logger.debug(logSystem, 'CALC', 'BTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/day per Mh/s'); + } + if (exchangeData.hasOwnProperty('LTC') && exchangeData['LTC'].hasOwnProperty('weightedBid')){ + var btcPerMhPerHour = (exchangeData['LTC'].weightedBid * coinStatus.coinsPerMhPerHour) * exchangeData['LTC'].ltcToBtc; + if (btcPerMhPerHour > bestBtcPerMhPerHour){ + bestBtcPerMhPerHour = btcPerMhPerHour; + bestExchange = exchange; + bestCoin = profitStatus[algo][symbol].name; + } + coinStatus.btcPerMhPerHour = btcPerMhPerHour; + logger.debug(logSystem, 'CALC', 'LTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/day per Mh/s'); + } + }); + }); + logger.debug(logSystem, 'RESULT', 'Best coin for ' + algo + ' is ' + bestCoin + ' on ' + bestExchange + ' with ' + bestBtcPerMhPerHour.toFixed(8) + ' BTC/day per Mh/s'); + + + var client = net.connect(portalConfig.cliPort, function () { + client.write(JSON.stringify({ + command: 'coinswitch', + params: [bestCoin], + options: {algorithm: algo} + }) + '\n'); + }).on('error', function(error){ + if (error.code === 'ECONNREFUSED') + logger.error(logSystem, 'CLI', 'Could not connect to NOMP instance on port ' + portalConfig.cliPort); + else + logger.error(logSystem, 'CLI', 'Socket error ' + JSON.stringify(error)); + }); + + }); + }; + + + var checkProfitability = function(){ + logger.debug(logSystem, 'Check', 'Collecting profitability data.'); + + profitabilityTasks = []; + if (portalConfig.profitSwitch.usePoloniex) + profitabilityTasks.push(_this.getProfitDataPoloniex); + + if (portalConfig.profitSwitch.useCryptsy) + profitabilityTasks.push(_this.getProfitDataCryptsy); + + if (portalConfig.profitSwitch.useMintpal) + profitabilityTasks.push(_this.getProfitDataMintpal); + + if (portalConfig.profitSwitch.useBittrex) + profitabilityTasks.push(_this.getProfitDataBittrex); + + profitabilityTasks.push(_this.getCoindDaemonInfo); + profitabilityTasks.push(_this.getMiningRate); + + // has to be series + async.series(profitabilityTasks, function(err){ + if (err){ + logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); + return; + } + // + // TODO offer support for a userConfigurable function for deciding on coin to override the default + // + _this.switchToMostProfitableCoins(); + }); + }; + setInterval(checkProfitability, portalConfig.profitSwitch.updateInterval * 1000); + +}; diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js new file mode 100644 index 0000000..43dd516 --- /dev/null +++ b/libs/shareProcessor.js @@ -0,0 +1,103 @@ +var redis = require('redis'); +var Stratum = require('stratum-pool'); + + + +/* +This module deals with handling shares when in internal payment processing mode. It connects to a redis +database and inserts shares with the database structure of: + +key: coin_name + ':' + block_height +value: a hash with.. + key: + + */ + + + +module.exports = function(logger, poolConfig){ + + var redisConfig = poolConfig.redis; + var coin = poolConfig.coin.name; + + + var forkId = process.env.forkId; + var logSystem = 'Pool'; + var logComponent = coin; + var logSubCat = 'Thread ' + (parseInt(forkId) + 1); + + var connection = redis.createClient(redisConfig.port, redisConfig.host); + if (redisConfig.password) { + connection.auth(redisConfig.password); + } + connection.on('ready', function(){ + logger.debug(logSystem, logComponent, logSubCat, 'Share processing setup with redis (' + redisConfig.host + + ':' + redisConfig.port + ')'); + }); + connection.on('error', function(err){ + logger.error(logSystem, logComponent, logSubCat, 'Redis client had an error: ' + JSON.stringify(err)) + }); + connection.on('end', function(){ + logger.error(logSystem, logComponent, logSubCat, 'Connection to redis database has been ended'); + }); + connection.info(function(error, response){ + if (error){ + logger.error(logSystem, logComponent, logSubCat, 'Redis version check failed'); + return; + } + var parts = response.split('\r\n'); + var version; + var versionString; + for (var i = 0; i < parts.length; i++){ + if (parts[i].indexOf(':') !== -1){ + var valParts = parts[i].split(':'); + if (valParts[0] === 'redis_version'){ + versionString = valParts[1]; + version = parseFloat(versionString); + break; + } + } + } + if (!version){ + logger.error(logSystem, logComponent, logSubCat, 'Could not detect redis version - but be super old or broken'); + } + else if (version < 2.6){ + logger.error(logSystem, logComponent, logSubCat, "You're using redis version " + versionString + " the minimum required version is 2.6. Follow the damn usage instructions..."); + } + }); + + this.handleShare = function(isValidShare, isValidBlock, shareData) { + + var redisCommands = []; + + if (isValidShare) { + redisCommands.push(['hincrbyfloat', coin + ':shares:roundCurrent', shareData.worker, shareData.difficulty]); + redisCommands.push(['hincrby', coin + ':stats', 'validShares', 1]); + } else { + redisCommands.push(['hincrby', coin + ':stats', 'invalidShares', 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. */ + var dateNow = Date.now(); + var hashrateData = [ isValidShare ? shareData.difficulty : -shareData.difficulty, shareData.worker, dateNow]; + redisCommands.push(['zadd', coin + ':hashrate', dateNow / 1000 | 0, hashrateData.join(':')]); + + if (isValidBlock){ + redisCommands.push(['rename', coin + ':shares:roundCurrent', coin + ':shares:round' + shareData.height]); + redisCommands.push(['rename', coin + ':shares:timesCurrent', coin + ':shares:times' + shareData.height]); + redisCommands.push(['sadd', coin + ':blocksPending', [shareData.blockHash, shareData.txHash, shareData.height, shareData.worker, dateNow].join(':')]); + redisCommands.push(['hincrby', coin + ':stats', 'validBlocks', 1]); + } + else if (shareData.blockHash){ + redisCommands.push(['hincrby', coin + ':stats', 'invalidBlocks', 1]); + } + + connection.multi(redisCommands).exec(function(err, replies){ + if (err) + logger.error(logSystem, logComponent, logSubCat, 'Error with share processor multi ' + JSON.stringify(err)); + }); + }; + +}; diff --git a/libs/stats.js b/libs/stats.js new file mode 100644 index 0000000..e2d568d --- /dev/null +++ b/libs/stats.js @@ -0,0 +1,774 @@ +var zlib = require('zlib'); + +var redis = require('redis'); +var async = require('async'); + +var os = require('os'); + +var algos = require('stratum-pool/lib/algoProperties.js'); + +// redis callback Ready check failed bypass trick +function rediscreateClient(port, host, pass) { + var client = redis.createClient(port, host); + if (pass) { + client.auth(pass); + } + return client; +} + + +/** + * Sort object properties (only own properties will be sorted). + * @param {object} obj object to sort properties + * @param {string|int} sortedBy 1 - sort object properties by specific value. + * @param {bool} isNumericSort true - sort object properties as numeric value, false - sort as string value. + * @param {bool} reverse false - reverse sorting. + * @returns {Array} array of items in [[key,value],[key,value],...] format. + */ +function sortProperties(obj, sortedBy, isNumericSort, reverse) { + sortedBy = sortedBy || 1; // by default first key + isNumericSort = isNumericSort || false; // by default text sort + reverse = reverse || false; // by default no reverse + + var reversed = (reverse) ? -1 : 1; + + var sortable = []; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + sortable.push([key, obj[key]]); + } + } + if (isNumericSort) + sortable.sort(function (a, b) { + return reversed * (a[1][sortedBy] - b[1][sortedBy]); + }); + else + sortable.sort(function (a, b) { + var x = a[1][sortedBy].toLowerCase(), + y = b[1][sortedBy].toLowerCase(); + return x < y ? reversed * -1 : x > y ? reversed : 0; + }); + return sortable; // array in format [ [ key1, val1 ], [ key2, val2 ], ... ] +} + +module.exports = function(logger, portalConfig, poolConfigs){ + + var _this = this; + + var logSystem = 'Stats'; + + var redisClients = []; + var redisStats; + + this.statHistory = []; + this.statPoolHistory = []; + + this.stats = {}; + this.statsString = ''; + + setupStatsRedis(); + gatherStatHistory(); + + var canDoStats = true; + + Object.keys(poolConfigs).forEach(function(coin){ + if (!canDoStats) return; + + var poolConfig = poolConfigs[coin]; + var redisConfig = poolConfig.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: rediscreateClient(redisConfig.port, redisConfig.host, redisConfig.password) + }); + }); + + function setupStatsRedis(){ + redisStats = redis.createClient(portalConfig.redis.port, portalConfig.redis.host); + redisStats.on('error', function(err){ + redisStats.auth(portalConfig.redis.password); + }); + } + + this.getBlocks = function (cback) { + var allBlocks = {}; + async.each(_this.stats.pools, function(pool, pcb) { + + if (_this.stats.pools[pool.name].pending && _this.stats.pools[pool.name].pending.blocks) + for (var i=0; i<_this.stats.pools[pool.name].pending.blocks.length; i++) + allBlocks[pool.name+"-"+_this.stats.pools[pool.name].pending.blocks[i].split(':')[2]] = _this.stats.pools[pool.name].pending.blocks[i]; + + if (_this.stats.pools[pool.name].confirmed && _this.stats.pools[pool.name].confirmed.blocks) + for (var i=0; i<_this.stats.pools[pool.name].confirmed.blocks.length; i++) + allBlocks[pool.name+"-"+_this.stats.pools[pool.name].confirmed.blocks[i].split(':')[2]] = _this.stats.pools[pool.name].confirmed.blocks[i]; + + pcb(); + }, function(err) { + cback(allBlocks); + }); + }; + + function gatherStatHistory(){ + var retentionTime = (((Date.now() / 1000) - portalConfig.website.stats.historicalRetention) | 0).toString(); + redisStats.zrangebyscore(['statHistory', retentionTime, '+inf'], function(err, replies){ + if (err) { + logger.error(logSystem, 'Historics', 'Error when trying to grab historical stats ' + JSON.stringify(err)); + return; + } + for (var i = 0; i < replies.length; i++){ + _this.statHistory.push(JSON.parse(replies[i])); + } + _this.statHistory = _this.statHistory.sort(function(a, b){ + return a.time - b.time; + }); + _this.statHistory.forEach(function(stats){ + addStatPoolHistory(stats); + }); + }); + } + + function getWorkerStats(address) { + address = address.split(".")[0]; + if (address.length > 0 && address.startsWith('t')) { + for (var h in statHistory) { + for(var pool in statHistory[h].pools) { + + statHistory[h].pools[pool].workers.sort(sortWorkersByHashrate); + + for(var w in statHistory[h].pools[pool].workers){ + if (w.startsWith(address)) { + if (history[w] == null) { + history[w] = []; + } + if (workers[w] == null && stats.pools[pool].workers[w] != null) { + workers[w] = stats.pools[pool].workers[w]; + } + if (statHistory[h].pools[pool].workers[w].hashrate) { + history[w].push({time: statHistory[h].time, hashrate:statHistory[h].pools[pool].workers[w].hashrate}); + } + } + } + } + } + return JSON.stringify({"workers": workers, "history": history}); + } + return null; + } + + function addStatPoolHistory(stats){ + var data = { + time: stats.time, + pools: {} + }; + for (var pool in stats.pools){ + data.pools[pool] = { + hashrate: stats.pools[pool].hashrate, + workerCount: stats.pools[pool].workerCount, + blocks: stats.pools[pool].blocks + } + } + _this.statPoolHistory.push(data); + } + + var magnitude = 100000000; + var coinPrecision = magnitude.toString().length - 1; + + function roundTo(n, digits) { + if (digits === undefined) { + digits = 0; + } + var multiplicator = Math.pow(10, digits); + n = parseFloat((n * multiplicator).toFixed(11)); + var test =(Math.round(n) / multiplicator); + return +(test.toFixed(digits)); + } + + var satoshisToCoins = function(satoshis){ + return roundTo((satoshis / magnitude), coinPrecision); + }; + + var coinsToSatoshies = function(coins){ + return Math.round(coins * magnitude); + }; + + function coinsRound(number) { + return roundTo(number, coinPrecision); + } + + function readableSeconds(t) { + var seconds = Math.round(t); + var minutes = Math.floor(seconds/60); + var hours = Math.floor(minutes/60); + var days = Math.floor(hours/24); + hours = hours-(days*24); + minutes = minutes-(days*24*60)-(hours*60); + seconds = seconds-(days*24*60*60)-(hours*60*60)-(minutes*60); + if (days > 0) { return (days + "d " + hours + "h " + minutes + "m " + seconds + "s"); } + if (hours > 0) { return (hours + "h " + minutes + "m " + seconds + "s"); } + if (minutes > 0) {return (minutes + "m " + seconds + "s"); } + return (seconds + "s"); + } + + this.getCoins = function(cback){ + _this.stats.coins = redisClients[0].coins; + cback(); + }; + + this.getPayout = function(address, cback){ + async.waterfall([ + function(callback){ + _this.getBalanceByAddress(address, function(){ + callback(null, 'test'); + }); + } + ], function(err, total){ + cback(coinsRound(total).toFixed(8)); + }); + }; + + this.getTotalSharesByAddress = function(address, cback) { + var a = address.split(".")[0]; + var client = redisClients[0].client, + coins = redisClients[0].coins, + shares = []; + + var pindex = parseInt(0); + var totalShares = parseFloat(0); + async.each(_this.stats.pools, function(pool, pcb) { + pindex++; + var coin = String(_this.stats.pools[pool.name].name); + client.hscan(coin + ':shares:roundCurrent', 0, "match", a+"*", "count", 1000, function(error, result) { + if (error) { + pcb(error); + return; + } + var workerName=""; + var shares = 0; + for (var i in result[1]) { + if (Math.abs(i % 2) != 1) { + workerName = String(result[1][i]); + } else { + shares += parseFloat(result[1][i]); + } + } + if (shares>0) { + totalShares = shares; + } + pcb(); + }); + }, function(err) { + if (err) { + cback(0); + return; + } + if (totalShares > 0 || (pindex >= Object.keys(_this.stats.pools).length)) { + cback(totalShares); + return; + } + }); + }; + + this.getBalanceByAddress = function(address, cback){ + + var a = address.split(".")[0]; + + var client = redisClients[0].client, + coins = redisClients[0].coins, + balances = []; + + var totalHeld = parseFloat(0); + var totalPaid = parseFloat(0); + var totalImmature = parseFloat(0); + + async.each(_this.stats.pools, function(pool, pcb) { + var coin = String(_this.stats.pools[pool.name].name); + // get all immature balances from address + client.hscan(coin + ':immature', 0, "match", a+"*", "count", 10000, function(error, pends) { + // get all balances from address + client.hscan(coin + ':balances', 0, "match", a+"*", "count", 10000, function(error, bals) { + // get all payouts from address + client.hscan(coin + ':payouts', 0, "match", a+"*", "count", 10000, function(error, pays) { + + var workerName = ""; + var balAmount = 0; + var paidAmount = 0; + var pendingAmount = 0; + + var workers = {}; + + for (var i in pays[1]) { + if (Math.abs(i % 2) != 1) { + workerName = String(pays[1][i]); + workers[workerName] = (workers[workerName] || {}); + } else { + paidAmount = parseFloat(pays[1][i]); + workers[workerName].paid = coinsRound(paidAmount); + totalPaid += paidAmount; + } + } + for (var b in bals[1]) { + if (Math.abs(b % 2) != 1) { + workerName = String(bals[1][b]); + workers[workerName] = (workers[workerName] || {}); + } else { + balAmount = parseFloat(bals[1][b]); + workers[workerName].balance = coinsRound(balAmount); + totalHeld += balAmount; + } + } + for (var b in pends[1]) { + if (Math.abs(b % 2) != 1) { + workerName = String(pends[1][b]); + workers[workerName] = (workers[workerName] || {}); + } else { + pendingAmount = parseFloat(pends[1][b]); + workers[workerName].immature = coinsRound(pendingAmount); + totalImmature += pendingAmount; + } + } + + for (var w in workers) { + balances.push({ + worker:String(w), + balance:workers[w].balance, + paid:workers[w].paid, + immature:workers[w].immature + }); + } + + pcb(); + }); + }); + }); + }, function(err) { + if (err) { + callback("There was an error getting balances"); + return; + } + + _this.stats.balances = balances; + _this.stats.address = address; + + cback({totalHeld:coinsRound(totalHeld), totalPaid:coinsRound(totalPaid), totalImmature:satoshisToCoins(totalImmature), balances}); + }); + }; + + this.getGlobalStats = function(callback){ + + var statGatherTime = Date.now() / 1000 | 0; + + var allCoinStats = {}; + + async.each(redisClients, function(client, callback){ + var windowTime = (((Date.now() / 1000) - portalConfig.website.stats.hashrateWindow) | 0).toString(); + var redisCommands = []; + + var redisCommandTemplates = [ + ['zremrangebyscore', ':hashrate', '-inf', '(' + windowTime], + ['zrangebyscore', ':hashrate', windowTime, '+inf'], + ['hgetall', ':stats'], + ['scard', ':blocksPending'], + ['scard', ':blocksConfirmed'], + ['scard', ':blocksKicked'], + ['smembers', ':blocksPending'], + ['smembers', ':blocksConfirmed'], + ['hgetall', ':shares:roundCurrent'], + ['hgetall', ':blocksPendingConfirms'], + ['zrange', ':payments', -100, -1], + ['hgetall', ':shares:timesCurrent'] + ]; + + var commandsPerCoin = redisCommandTemplates.length; + + client.coins.map(function(coin){ + redisCommandTemplates.map(function(t){ + var clonedTemplates = t.slice(0); + clonedTemplates[1] = coin + clonedTemplates[1]; + redisCommands.push(clonedTemplates); + }); + }); + + client.client.multi(redisCommands).exec(function(err, replies){ + if (err){ + logger.error(logSystem, 'Global', 'error with getting global stats ' + JSON.stringify(err)); + callback(err); + } + else{ + for(var i = 0; i < replies.length; i += commandsPerCoin){ + var coinName = client.coins[i / commandsPerCoin | 0]; + var marketStats = {}; + if (replies[i + 2]) { + if (replies[i + 2].coinmarketcap) { + marketStats = replies[i + 2] ? (JSON.parse(replies[i + 2].coinmarketcap)[0] || 0) : 0; + } + } + var coinStats = { + name: coinName, + symbol: poolConfigs[coinName].coin.symbol.toUpperCase(), + algorithm: poolConfigs[coinName].coin.algorithm, + hashrates: replies[i + 1], + poolStats: { + validShares: replies[i + 2] ? (replies[i + 2].validShares || 0) : 0, + validBlocks: replies[i + 2] ? (replies[i + 2].validBlocks || 0) : 0, + invalidShares: replies[i + 2] ? (replies[i + 2].invalidShares || 0) : 0, + totalPaid: replies[i + 2] ? (replies[i + 2].totalPaid || 0) : 0, + networkBlocks: replies[i + 2] ? (replies[i + 2].networkBlocks || 0) : 0, + networkSols: replies[i + 2] ? (replies[i + 2].networkSols || 0) : 0, + networkSolsString: getReadableNetworkHashRateString(replies[i + 2] ? (replies[i + 2].networkSols || 0) : 0), + networkDiff: replies[i + 2] ? (replies[i + 2].networkDiff || 0) : 0, + networkConnections: replies[i + 2] ? (replies[i + 2].networkConnections || 0) : 0, + networkVersion: replies[i + 2] ? (replies[i + 2].networkSubVersion || 0) : 0, + networkProtocolVersion: replies[i + 2] ? (replies[i + 2].networkProtocolVersion || 0) : 0 + }, + marketStats: marketStats, + /* block stat counts */ + blocks: { + pending: replies[i + 3], + confirmed: replies[i + 4], + orphaned: replies[i + 5] + }, + /* show all pending blocks */ + pending: { + blocks: replies[i + 6].sort(sortBlocks), + confirms: (replies[i + 9] || {}) + }, + /* show last 50 found blocks */ + confirmed: { + blocks: replies[i + 7].sort(sortBlocks).slice(0,50) + }, + payments: [], + currentRoundShares: (replies[i + 8] || {}), + currentRoundTimes: (replies[i + 11] || {}), + maxRoundTime: 0, + shareCount: 0 + }; + for(var j = replies[i + 10].length; j > 0; j--){ + var jsonObj; + try { + jsonObj = JSON.parse(replies[i + 10][j-1]); + } catch(e) { + jsonObj = null; + } + if (jsonObj !== null) { + coinStats.payments.push(jsonObj); + } + } + allCoinStats[coinStats.name] = (coinStats); + } + // sort pools alphabetically + allCoinStats = sortPoolsByName(allCoinStats); + callback(); + } + }); + }, function(err){ + if (err){ + logger.error(logSystem, 'Global', 'error getting all stats' + JSON.stringify(err)); + callback(); + return; + } + + var portalStats = { + time: statGatherTime, + global:{ + workers: 0, + hashrate: 0 + }, + algos: {}, + pools: allCoinStats + }; + + Object.keys(allCoinStats).forEach(function(coin){ + var coinStats = allCoinStats[coin]; + coinStats.workers = {}; + coinStats.miners = {}; + coinStats.shares = 0; + coinStats.hashrates.forEach(function(ins){ + var parts = ins.split(':'); + var workerShares = parseFloat(parts[0]); + var miner = parts[1].split('.')[0]; + var worker = parts[1]; + var diff = Math.round(parts[0] * 8192); + if (workerShares > 0) { + coinStats.shares += workerShares; + // build worker stats + if (worker in coinStats.workers) { + coinStats.workers[worker].shares += workerShares; + coinStats.workers[worker].diff = diff; + } else { + coinStats.workers[worker] = { + name: worker, + diff: diff, + shares: workerShares, + invalidshares: 0, + currRoundShares: 0, + currRoundTime: 0, + hashrate: null, + hashrateString: null, + luckDays: null, + luckHours: null, + paid: 0, + balance: 0 + }; + } + // build miner stats + if (miner in coinStats.miners) { + coinStats.miners[miner].shares += workerShares; + } else { + coinStats.miners[miner] = { + name: miner, + shares: workerShares, + invalidshares: 0, + currRoundShares: 0, + currRoundTime: 0, + hashrate: null, + hashrateString: null, + luckDays: null, + luckHours: null + }; + } + } + else { + // build worker stats + if (worker in coinStats.workers) { + coinStats.workers[worker].invalidshares -= workerShares; // workerShares is negative number! + coinStats.workers[worker].diff = diff; + } else { + coinStats.workers[worker] = { + name: worker, + diff: diff, + shares: 0, + invalidshares: -workerShares, + currRoundShares: 0, + currRoundTime: 0, + hashrate: null, + hashrateString: null, + luckDays: null, + luckHours: null, + paid: 0, + balance: 0 + }; + } + // build miner stats + if (miner in coinStats.miners) { + coinStats.miners[miner].invalidshares -= workerShares; // workerShares is negative number! + } else { + coinStats.miners[miner] = { + name: miner, + shares: 0, + invalidshares: -workerShares, + currRoundShares: 0, + currRoundTime: 0, + hashrate: null, + hashrateString: null, + luckDays: null, + luckHours: null + }; + } + } + }); + + // sort miners + coinStats.miners = sortMinersByHashrate(coinStats.miners); + + var shareMultiplier = Math.pow(2, 32) / algos[coinStats.algorithm].multiplier; + coinStats.hashrate = shareMultiplier * coinStats.shares / portalConfig.website.stats.hashrateWindow; + coinStats.hashrateString = _this.getReadableHashRateString(coinStats.hashrate); + + var _blocktime = 160; + var _networkHashRate = parseFloat(coinStats.poolStats.networkSols) * 1.2; + var _myHashRate = (coinStats.hashrate / 1000000) * 2; + coinStats.luckDays = ((_networkHashRate / _myHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3); + coinStats.luckHours = ((_networkHashRate / _myHashRate * _blocktime) / (60 * 60)).toFixed(3); + coinStats.minerCount = Object.keys(coinStats.miners).length; + coinStats.workerCount = Object.keys(coinStats.workers).length; + portalStats.global.workers += coinStats.workerCount; + + /* algorithm specific global stats */ + var algo = coinStats.algorithm; + if (!portalStats.algos.hasOwnProperty(algo)){ + portalStats.algos[algo] = { + workers: 0, + hashrate: 0, + hashrateString: null + }; + } + portalStats.algos[algo].hashrate += coinStats.hashrate; + portalStats.algos[algo].workers += Object.keys(coinStats.workers).length; + + var _shareTotal = parseFloat(0); + var _maxTimeShare = parseFloat(0); + for (var worker in coinStats.currentRoundShares) { + var miner = worker.split(".")[0]; + if (miner in coinStats.miners) { + coinStats.miners[miner].currRoundShares += parseFloat(coinStats.currentRoundShares[worker]); + } + if (worker in coinStats.workers) { + coinStats.workers[worker].currRoundShares += parseFloat(coinStats.currentRoundShares[worker]); + } + _shareTotal += parseFloat(coinStats.currentRoundShares[worker]); + } + for (var worker in coinStats.currentRoundTimes) { + var time = parseFloat(coinStats.currentRoundTimes[worker]); + if (_maxTimeShare < time) { _maxTimeShare = time; } + var miner = worker.split(".")[0]; // split poolId from minerAddress + if (miner in coinStats.miners && coinStats.miners[miner].currRoundTime < time) { + coinStats.miners[miner].currRoundTime = time; + } + } + + coinStats.shareCount = _shareTotal; + coinStats.maxRoundTime = _maxTimeShare; + coinStats.maxRoundTimeString = readableSeconds(_maxTimeShare); + + for (var worker in coinStats.workers) { + var _workerRate = shareMultiplier * coinStats.workers[worker].shares / portalConfig.website.stats.hashrateWindow; + var _wHashRate = (_workerRate / 1000000) * 2; + coinStats.workers[worker].luckDays = ((_networkHashRate / _wHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3); + coinStats.workers[worker].luckHours = ((_networkHashRate / _wHashRate * _blocktime) / (60 * 60)).toFixed(3); + coinStats.workers[worker].hashrate = _workerRate; + coinStats.workers[worker].hashrateString = _this.getReadableHashRateString(_workerRate); + var miner = worker.split('.')[0]; + if (miner in coinStats.miners) { + coinStats.workers[worker].currRoundTime = coinStats.miners[miner].currRoundTime; + } + } + for (var miner in coinStats.miners) { + var _workerRate = shareMultiplier * coinStats.miners[miner].shares / portalConfig.website.stats.hashrateWindow; + var _wHashRate = (_workerRate / 1000000) * 2; + coinStats.miners[miner].luckDays = ((_networkHashRate / _wHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3); + coinStats.miners[miner].luckHours = ((_networkHashRate / _wHashRate * _blocktime) / (60 * 60)).toFixed(3); + coinStats.miners[miner].hashrate = _workerRate; + coinStats.miners[miner].hashrateString = _this.getReadableHashRateString(_workerRate); + } + + // sort workers by name + coinStats.workers = sortWorkersByName(coinStats.workers); + + delete coinStats.hashrates; + delete coinStats.shares; + }); + + Object.keys(portalStats.algos).forEach(function(algo){ + var algoStats = portalStats.algos[algo]; + algoStats.hashrateString = _this.getReadableHashRateString(algoStats.hashrate); + }); + + _this.stats = portalStats; + + // save historical hashrate, not entire stats! + var saveStats = JSON.parse(JSON.stringify(portalStats)); + Object.keys(saveStats.pools).forEach(function(pool){ + delete saveStats.pools[pool].pending; + delete saveStats.pools[pool].confirmed; + delete saveStats.pools[pool].currentRoundShares; + delete saveStats.pools[pool].currentRoundTimes; + delete saveStats.pools[pool].payments; + delete saveStats.pools[pool].miners; + }); + _this.statsString = JSON.stringify(saveStats); + _this.statHistory.push(saveStats); + + addStatPoolHistory(portalStats); + + var retentionTime = (((Date.now() / 1000) - portalConfig.website.stats.historicalRetention) | 0); + + for (var i = 0; i < _this.statHistory.length; i++){ + if (retentionTime < _this.statHistory[i].time){ + if (i > 0) { + _this.statHistory = _this.statHistory.slice(i); + _this.statPoolHistory = _this.statPoolHistory.slice(i); + } + break; + } + } + + redisStats.multi([ + ['zadd', 'statHistory', statGatherTime, _this.statsString], + ['zremrangebyscore', 'statHistory', '-inf', '(' + retentionTime] + ]).exec(function(err, replies){ + if (err) + logger.error(logSystem, 'Historics', 'Error adding stats to historics ' + JSON.stringify(err)); + }); + callback(); + }); + + }; + + function sortPoolsByName(objects) { + var newObject = {}; + var sortedArray = sortProperties(objects, 'name', false, false); + for (var i = 0; i < sortedArray.length; i++) { + var key = sortedArray[i][0]; + var value = sortedArray[i][1]; + newObject[key] = value; + } + return newObject; + } + + function sortBlocks(a, b) { + var as = parseInt(a.split(":")[2]); + var bs = parseInt(b.split(":")[2]); + if (as > bs) return -1; + if (as < bs) return 1; + return 0; + } + + function sortWorkersByName(objects) { + var newObject = {}; + var sortedArray = sortProperties(objects, 'name', false, false); + for (var i = 0; i < sortedArray.length; i++) { + var key = sortedArray[i][0]; + var value = sortedArray[i][1]; + newObject[key] = value; + } + return newObject; + } + + function sortMinersByHashrate(objects) { + var newObject = {}; + var sortedArray = sortProperties(objects, 'shares', true, true); + for (var i = 0; i < sortedArray.length; i++) { + var key = sortedArray[i][0]; + var value = sortedArray[i][1]; + newObject[key] = value; + } + return newObject; + } + + function sortWorkersByHashrate(a, b) { + if (a.hashrate === b.hashrate) { + return 0; + } + else { + return (a.hashrate < b.hashrate) ? -1 : 1; + } + } + + this.getReadableHashRateString = function(hashrate){ + hashrate = (hashrate * 2); + if (hashrate < 1000000) { + return (Math.round(hashrate / 1000) / 1000 ).toFixed(2)+' Sol/s'; + } + var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ]; + var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1); + hashrate = (hashrate/1000) / Math.pow(1000, i + 1); + return hashrate.toFixed(2) + byteUnits[i]; + }; + + function getReadableNetworkHashRateString(hashrate) { + hashrate = (hashrate * 1000000); + if (hashrate < 1000000) + return '0 Sol'; + var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ]; + var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1); + hashrate = (hashrate/1000) / Math.pow(1000, i + 1); + return hashrate.toFixed(2) + byteUnits[i]; + } +}; diff --git a/libs/website.js b/libs/website.js new file mode 100644 index 0000000..af956c9 --- /dev/null +++ b/libs/website.js @@ -0,0 +1,362 @@ +var https = require('https'); +var fs = require('fs'); +var path = require('path'); + +var async = require('async'); +var watch = require('node-watch'); +var redis = require('redis'); + +var dot = require('dot'); +var express = require('express'); +var bodyParser = require('body-parser'); +var compress = require('compression'); + +var Stratum = require('stratum-pool'); +var util = require('stratum-pool/lib/util.js'); + +var api = require('./api.js'); + + +module.exports = function(logger){ + + dot.templateSettings.strip = false; + + var portalConfig = JSON.parse(process.env.portalConfig); + var poolConfigs = JSON.parse(process.env.pools); + + var websiteConfig = portalConfig.website; + + var portalApi = new api(logger, portalConfig, poolConfigs); + var portalStats = portalApi.stats; + + var logSystem = 'Website'; + + + var pageFiles = { + 'index.html': 'index', + 'home.html': '', + 'getting_started.html': 'getting_started', + 'stats.html': 'stats', + 'tbs.html': 'tbs', + 'workers.html': 'workers', + 'api.html': 'api', + 'admin.html': 'admin', + 'mining_key.html': 'mining_key', + 'miner_stats.html': 'miner_stats', + 'payments.html': 'payments' + }; + + var pageTemplates = {}; + + var pageProcessed = {}; + var indexesProcessed = {}; + + var keyScriptTemplate = ''; + var keyScriptProcessed = ''; + + var processTemplates = function(){ + + for (var pageName in pageTemplates){ + if (pageName === 'index') continue; + pageProcessed[pageName] = pageTemplates[pageName]({ + poolsConfigs: poolConfigs, + stats: portalStats.stats, + portalConfig: portalConfig + }); + indexesProcessed[pageName] = pageTemplates.index({ + page: pageProcessed[pageName], + selected: pageName, + stats: portalStats.stats, + poolConfigs: poolConfigs, + portalConfig: portalConfig + }); + } + + //logger.debug(logSystem, 'Stats', 'Website updated to latest stats'); + }; + + + + var readPageFiles = function(files){ + async.each(files, function(fileName, callback){ + var filePath = 'website/' + (fileName === 'index.html' ? '' : 'pages/') + fileName; + fs.readFile(filePath, 'utf8', function(err, data){ + var pTemp = dot.template(data); + pageTemplates[pageFiles[fileName]] = pTemp + callback(); + }); + }, function(err){ + if (err){ + console.log('error reading files for creating dot templates: '+ JSON.stringify(err)); + return; + } + processTemplates(); + }); + }; + + + // if an html file was changed reload it + /* requires node-watch 0.5.0 or newer */ + watch(['./website', './website/pages'], function(evt, filename){ + var basename; + // support older versions of node-watch automatically + if (!filename && evt) + basename = path.basename(evt); + else + basename = path.basename(filename); + + if (basename in pageFiles){ + readPageFiles([basename]); + logger.special(logSystem, 'Server', 'Reloaded file ' + basename); + } + }); + + portalStats.getGlobalStats(function(){ + readPageFiles(Object.keys(pageFiles)); + }); + + var buildUpdatedWebsite = function(){ + portalStats.getGlobalStats(function(){ + processTemplates(); + + var statData = 'data: ' + JSON.stringify(portalStats.stats) + '\n\n'; + for (var uid in portalApi.liveStatConnections){ + var res = portalApi.liveStatConnections[uid]; + res.write(statData); + } + + }); + }; + + setInterval(buildUpdatedWebsite, websiteConfig.stats.updateInterval * 1000); + + var buildKeyScriptPage = function(){ + async.waterfall([ + function(callback){ + var client = redis.createClient(portalConfig.redis.port, portalConfig.redis.host); + if (portalConfig.redis.password) { + client.auth(portalConfig.redis.password); + } + client.hgetall('coinVersionBytes', function(err, coinBytes){ + if (err){ + client.quit(); + return callback('Failed grabbing coin version bytes from redis ' + JSON.stringify(err)); + } + callback(null, client, coinBytes || {}); + }); + }, + function (client, coinBytes, callback){ + var enabledCoins = Object.keys(poolConfigs).map(function(c){return c.toLowerCase()}); + var missingCoins = []; + enabledCoins.forEach(function(c){ + if (!(c in coinBytes)) + missingCoins.push(c); + }); + callback(null, client, coinBytes, missingCoins); + }, + function(client, coinBytes, missingCoins, callback){ + var coinsForRedis = {}; + async.each(missingCoins, function(c, cback){ + var coinInfo = (function(){ + for (var pName in poolConfigs){ + if (pName.toLowerCase() === c) + return { + daemon: poolConfigs[pName].paymentProcessing.daemon, + address: poolConfigs[pName].address + } + } + })(); + var daemon = new Stratum.daemon.interface([coinInfo.daemon], function(severity, message){ + logger[severity](logSystem, c, message); + }); + daemon.cmd('dumpprivkey', [coinInfo.address], function(result){ + if (result[0].error){ + logger.error(logSystem, c, 'Could not dumpprivkey for ' + c + ' ' + JSON.stringify(result[0].error)); + cback(); + return; + } + + var vBytePub = util.getVersionByte(coinInfo.address)[0]; + var vBytePriv = util.getVersionByte(result[0].response)[0]; + + coinBytes[c] = vBytePub.toString() + ',' + vBytePriv.toString(); + coinsForRedis[c] = coinBytes[c]; + cback(); + }); + }, function(err){ + callback(null, client, coinBytes, coinsForRedis); + }); + }, + function(client, coinBytes, coinsForRedis, callback){ + if (Object.keys(coinsForRedis).length > 0){ + client.hmset('coinVersionBytes', coinsForRedis, function(err){ + if (err) + logger.error(logSystem, 'Init', 'Failed inserting coin byte version into redis ' + JSON.stringify(err)); + client.quit(); + }); + } + else{ + client.quit(); + } + callback(null, coinBytes); + } + ], function(err, coinBytes){ + if (err){ + logger.error(logSystem, 'Init', err); + return; + } + try{ + keyScriptTemplate = dot.template(fs.readFileSync('website/key.html', {encoding: 'utf8'})); + keyScriptProcessed = keyScriptTemplate({coins: coinBytes}); + } + catch(e){ + logger.error(logSystem, 'Init', 'Failed to read key.html file'); + } + }); + + }; + buildKeyScriptPage(); + + var getPage = function(pageId){ + if (pageId in pageProcessed){ + var requestedPage = pageProcessed[pageId]; + return requestedPage; + } + }; + + var minerpage = function(req, res, next){ + var address = req.params.address || null; + if (address != null) { + address = address.split(".")[0]; + portalStats.getBalanceByAddress(address, function(){ + processTemplates(); + res.header('Content-Type', 'text/html'); + res.end(indexesProcessed['miner_stats']); + }); + } + else + next(); + }; + + var payout = function(req, res, next){ + var address = req.params.address || null; + if (address != null){ + portalStats.getPayout(address, function(data){ + res.write(data.toString()); + res.end(); + }); + } + else + next(); + }; + + var shares = function(req, res, next){ + portalStats.getCoins(function(){ + processTemplates(); + res.end(indexesProcessed['user_shares']); + + }); + }; + + var usershares = function(req, res, next){ + var coin = req.params.coin || null; + if(coin != null){ + portalStats.getCoinTotals(coin, null, function(){ + processTemplates(); + res.end(indexesProcessed['user_shares']); + }); + } + else + next(); + }; + + var route = function(req, res, next){ + var pageId = req.params.page || ''; + if (pageId in indexesProcessed){ + res.header('Content-Type', 'text/html'); + res.end(indexesProcessed[pageId]); + } + else + next(); + + }; + + + + var app = express(); + + + app.use(bodyParser.json()); + + app.get('/get_page', function(req, res, next){ + var requestedPage = getPage(req.query.id); + if (requestedPage){ + res.end(requestedPage); + return; + } + next(); + }); + + app.get('/key.html', function(req, res, next){ + res.end(keyScriptProcessed); + }); + + //app.get('/stats/shares/:coin', usershares); + //app.get('/stats/shares', shares); + //app.get('/payout/:address', payout); + app.use(compress()); + app.get('/workers/:address', minerpage); + app.get('/:page', route); + app.get('/', route); + + app.get('/api/:method', function(req, res, next){ + portalApi.handleApiRequest(req, res, next); + }); + + app.post('/api/admin/:method', function(req, res, next){ + if (portalConfig.website + && portalConfig.website.adminCenter + && portalConfig.website.adminCenter.enabled){ + if (portalConfig.website.adminCenter.password === req.body.password) + portalApi.handleAdminApiRequest(req, res, next); + else + res.send(401, JSON.stringify({error: 'Incorrect Password'})); + + } + else + next(); + + }); + + app.use(compress()); + app.use('/static', express.static('website/static')); + + app.use(function(err, req, res, next){ + console.error(err.stack); + res.send(500, 'Something broke!'); + }); + + try { + if (portalConfig.website.tlsOptions && portalConfig.website.tlsOptions.enabled === true) { + var TLSoptions = { + key: fs.readFileSync(portalConfig.website.tlsOptions.key), + cert: fs.readFileSync(portalConfig.website.tlsOptions.cert) + }; + + https.createServer(TLSoptions, app).listen(portalConfig.website.port, portalConfig.website.host, function() { + logger.debug(logSystem, 'Server', 'TLS Website started on ' + portalConfig.website.host + ':' + portalConfig.website.port); + }); + } else { + app.listen(portalConfig.website.port, portalConfig.website.host, function () { + logger.debug(logSystem, 'Server', 'Website started on ' + portalConfig.website.host + ':' + portalConfig.website.port); + }); + } + } + catch(e){ + console.log(e) + logger.error(logSystem, 'Server', 'Could not start website on ' + portalConfig.website.host + ':' + portalConfig.website.port + + ' - its either in use or you do not have permission'); + } + + +}; diff --git a/libs/workerapi.js b/libs/workerapi.js new file mode 100644 index 0000000..9e1f732 --- /dev/null +++ b/libs/workerapi.js @@ -0,0 +1,56 @@ +var express = require('express'); +var os = require('os'); + + +function workerapi(listen) { + var _this = this; + var app = express(); + var counters = { + validShares : 0, + validBlocks : 0, + invalidShares : 0 + }; + + var lastEvents = { + lastValidShare : 0 , + lastValidBlock : 0, + lastInvalidShare : 0 + }; + + app.get('/stats', function (req, res) { + res.send({ + "clients" : Object.keys(_this.poolObj.stratumServer.getStratumClients()).length, + "counters" : counters, + "lastEvents" : lastEvents + }); + }); + + + this.start = function (poolObj) { + this.poolObj = poolObj; + this.poolObj.once('started', function () { + app.listen(listen, function (lol) { + console.log("LISTENING "); + }); + }) + .on('share', function(isValidShare, isValidBlock, shareData) { + var now = Date.now(); + if (isValidShare) { + counters.validShares ++; + lastEvents.lastValidShare = now; + if (isValidBlock) { + counters.validBlocks ++; + lastEvents.lastValidBlock = now; + } + } else { + counters.invalidShares ++; + lastEvents.lastInvalidShare = now; + } + }); + } +} + + + +module.exports = workerapi; + diff --git a/package.json b/package.json new file mode 100644 index 0000000..a9233cc --- /dev/null +++ b/package.json @@ -0,0 +1,53 @@ +{ + "name": "Komodo-mining", + "version": "0.0.1", + "description": "High performance Stratum poolserver in Node.js", + "keywords": [ + "stratum", + "mining", + "pool", + "poolserver", + "komodo" + ], + "homepage": "https://github.com/TheComputerGenie/Knomp", + "bugs": { + "url": "https://github.com/TheComputerGenie/Knomp/issues" + }, + "license": "GPL-2.0", + "author": "ComputerGenie", + "contributors": [ + "z-classic" + ], + "main": "init.js", + "bin": { + "block-notify": "./scripts/blockNotify.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/TheComputerGenie/Knomp.git" + }, + "dependencies": { + "async": "2.3.0", + "bignum": "0.12.5", + "body-parser": "1.17.1", + "colors": "1.1.2", + "compression": "1.6.2", + "dateformat": "2.0.0", + "dot": "1.1.1", + "express": "4.15.2", + "extend": "3.0.0", + "mysql": "2.13.0", + "node-json-minify": "1.0.0", + "node-watch": "0.5.2", + "nonce": "1.0.4", + "redis": "2.7.1", + "request": "2.81.0", + "stratum-pool": "git+https://github.com/TheComputerGenie/node-stratum-pool.git" + }, + "engines": { + "node": ">=0.10" + }, + "scripts": { + "start": "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/node_modules/stratum-pool/node_modules/equihashverify/build/Release/:$PWD/node_modules/equihashverify/build/Release/ node init.js" + } +} diff --git a/scripts/blocknotify b/scripts/blocknotify new file mode 100755 index 0000000..6138c66 Binary files /dev/null and b/scripts/blocknotify differ diff --git a/scripts/blocknotify.c b/scripts/blocknotify.c new file mode 100644 index 0000000..a119096 --- /dev/null +++ b/scripts/blocknotify.c @@ -0,0 +1,84 @@ +#include +#include +#include +#include +#include +#include +#include + +/* + +Contributed by Alex Petrov aka SysMan at sysman.net +Updated by Alejandro Reyero - TodoJuegos.com + +Part of NOMP project +Simple lightweight & fast - a more efficient block notify script in pure C. + +(may also work as coin switch) + +Platforms : Linux, BSD, Solaris (mostly OS independent) + +Build with: + gcc blocknotify.c -o blocknotify + + +Example usage in daemon coin.conf using default NOMP CLI port of 17117 + blocknotify="/bin/blocknotify 127.0.0.1:17117 dogecoin %s" + + + +*/ + + +int main(int argc, char **argv) +{ + int sockfd,n; + struct sockaddr_in servaddr, cliaddr; + char sendline[1000]; + char recvline[1000]; + char host[200]; + char *p, *arg, *errptr; + int port; + + if (argc < 3) + { + // print help + printf("NOMP pool block notify\n usage: \n"); + exit(1); + } + + strncpy(host, argv[1], (sizeof(host)-1)); + p = host; + + if ( (arg = strchr(p,':')) ) + { + *arg = '\0'; + + errno = 0; // reset errno + port = strtol(++arg, &errptr, 10); + + if ( (errno != 0) || (errptr == arg) ) + { + fprintf(stderr, "port number fail [%s]\n", errptr); + } + + } + + snprintf(sendline, sizeof(sendline) - 1, "{\"command\":\"blocknotify\",\"params\":[\"%s\",\"%s\"]}\n", argv[2], argv[3]); + + sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + bzero(&servaddr, sizeof(servaddr)); + servaddr.sin_family = AF_INET; + servaddr.sin_addr.s_addr = inet_addr(host); + servaddr.sin_port = htons(port); + connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); + + int result = send(sockfd, sendline, strlen(sendline), 0); + close(sockfd); + + if(result == -1) { + printf("Error sending: %i\n", errno); + exit(-1); + } + exit(0); +} diff --git a/scripts/cli.js b/scripts/cli.js new file mode 100644 index 0000000..5e1cfd6 --- /dev/null +++ b/scripts/cli.js @@ -0,0 +1,37 @@ +var net = require('net'); + +var defaultPort = 17117; +var defaultHost = '127.0.0.1'; + +var args = process.argv.slice(2); +var params = []; +var options = {}; + +for(var i = 0; i < args.length; i++){ + if (args[i].indexOf('-') === 0 && args[i].indexOf('=') !== -1){ + var s = args[i].substr(1).split('='); + options[s[0]] = s[1]; + } + else + params.push(args[i]); +} + +var command = params.shift(); + + + +var client = net.connect(options.port || defaultPort, options.host || defaultHost, function () { + client.write(JSON.stringify({ + command: command, + params: params, + options: options + }) + '\n'); +}).on('error', function(error){ + if (error.code === 'ECONNREFUSED') + console.log('Could not connect to NOMP instance at ' + defaultHost + ':' + defaultPort); + else + console.log('Socket error ' + JSON.stringify(error)); +}).on('data', function(data) { + console.log(data.toString()); +}).on('close', function () { +}); \ No newline at end of file diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..88a5153 --- /dev/null +++ b/website/index.html @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + z-nomp + + + + + +
+ +
+ +
+ {{=it.page}} +
+ + +
+ +
+ This site is powered by the open source z-nomp + project created by Joshua Yabut and the Zclassic Community and is licensed under the MIT License +
+
+   Support this project by donating  BTC: 18vHMxVzotQ9EPyESrf7Z1hNM9AwJeVHgD +
+
+ Community  :  #zclassic IRC +   |   + /r/zclassic +   |   + +
+ +
+ + + diff --git a/website/key.html b/website/key.html new file mode 100644 index 0000000..9978e9e --- /dev/null +++ b/website/key.html @@ -0,0 +1,2798 @@ + + + + + + Mining Key Script + + + + + + + + +
+

Mining key generation or input options:

+ +
+
1)
+
Create new private key
+ +
+
- or-
+
+
2)
+
Import existing private key
+ +
+
- or-
+
+
3)
+
Input private key hex
+ + +
Private key must be 64 hexadecimal characters
+
+
+ +
+ + + +
+
NO NOT LOSE THIS PRIVATE KEY. Any coins mined using this public key can + only be controlled with this private key.
+ +
Private key:
+
Key for mining (hashed public key):
+ +

+ An address for any type of coin can be derived from this mining key - and each of those coin address + can only be controlled by this private key. +

+ +
+ +
+
Backup your private key
+ + + + + + + + + +
Step 1) + +
Step 2) + +
+
+ +
+
Coin formatted keys
+ + +
Public address
+ + +
Private key in wallet import format
+ + +
+
How to import your private key for :
+
    +
  1. Open your wallet app
  2. +
  3. Go to Help -> click Debug window -> click Console tab
  4. +
  5. Enter the following command: importprivkey
  6. +
+
+ +
+ + +
+ + + + + + + + + \ No newline at end of file diff --git a/website/pages/admin.html b/website/pages/admin.html new file mode 100644 index 0000000..b752a0c --- /dev/null +++ b/website/pages/admin.html @@ -0,0 +1,50 @@ +
+ + + +
+
+ Password + + + + + + +
+
+ +
+ +
+ Administration + +
+ +
+ +
+ + + + +
\ No newline at end of file diff --git a/website/pages/api.html b/website/pages/api.html new file mode 100644 index 0000000..c84a4db --- /dev/null +++ b/website/pages/api.html @@ -0,0 +1,13 @@ +
+ API - The API is work in progress and is subject to change during development. + + +
diff --git a/website/pages/getting_started.html b/website/pages/getting_started.html new file mode 100644 index 0000000..a563a68 --- /dev/null +++ b/website/pages/getting_started.html @@ -0,0 +1,318 @@ + + +
+ + + + +
+ + + + + + diff --git a/website/pages/home.html b/website/pages/home.html new file mode 100644 index 0000000..2aca416 --- /dev/null +++ b/website/pages/home.html @@ -0,0 +1,140 @@ + + + +
+
+ +
+
+
Welcome to the future of mining
+
    +
  • Low fees
  • +
  • High performance Node.js backend
  • +
  • User friendly mining client
  • +
  • Multi-coin / multi-pool
  • +
+
+
+ +
+ +
+
+
Global Stats
+
+ {{ for(var algo in it.stats.algos) { }} +
+
{{=algo}}
+
{{=it.stats.algos[algo].workers}} Miners
+
{{=it.stats.algos[algo].hashrateString}}
+
+ {{ } }} +
+
+
+ +
+
+
Pools / Coins
+
+ {{ for(var pool in it.stats.pools) { }} +
+
{{=pool}}
+
{{=it.stats.pools[pool].workerCount}} Miners
+
{{=it.stats.pools[pool].hashrateString}}
+
+ {{ } }} +
+
+
+ +
+ + \ No newline at end of file diff --git a/website/pages/miner_stats.html b/website/pages/miner_stats.html new file mode 100644 index 0000000..250c2ec --- /dev/null +++ b/website/pages/miner_stats.html @@ -0,0 +1,105 @@ + + +
+
+
+ +
{{=String(it.stats.address).split(".")[0]}}
+
... (Avg)
+
... (Now)
+
Luck ... Days
+
+
+
+
Shares: ...
+
Immature: ...
+
Bal: ...
+
Paid: ...
+
+
+
+ +
+ + diff --git a/website/pages/mining_key.html b/website/pages/mining_key.html new file mode 100644 index 0000000..d380676 --- /dev/null +++ b/website/pages/mining_key.html @@ -0,0 +1,25 @@ + + +
+ +

+ This script run client-side (in your browser). For maximum security download the script and run it locally and + offline in a modern web browser. +

+ + + +
diff --git a/website/pages/payments.html b/website/pages/payments.html new file mode 100644 index 0000000..74aa2ff --- /dev/null +++ b/website/pages/payments.html @@ -0,0 +1,93 @@ + + +{{ function readableDate(a){ return new Date(parseInt(a)).toString(); } }} +{{ for(var pool in it.stats.pools) { }} + + + + + + + + + + + {{ for(var p in it.stats.pools[pool].payments) { }} + + + + + + + + {{ } }} +
BlocksTimeMinersSharesAmount
+ {{if (String(it.stats.pools[pool].name).startsWith("zcash")) { }} + {{=it.stats.pools[pool].payments[p].blocks}} + {{ } else if (String(it.stats.pools[pool].name).startsWith("zclassic")) { }} + {{=it.stats.pools[pool].payments[p].blocks}} + {{ } else if (String(it.stats.pools[pool].name).startsWith("hush")) { }} + {{=it.stats.pools[pool].payments[p].blocks}} + {{ } else if (String(it.stats.pools[pool].name).startsWith("zen")) { }} + {{=it.stats.pools[pool].payments[p].blocks}} + {{ } else { }} + {{=it.stats.pools[pool].payments[p].blocks}} + {{ } }} + {{=readableDate(it.stats.pools[pool].payments[p].time)}}{{=it.stats.pools[pool].payments[p].miners}}{{=Math.round(it.stats.pools[pool].payments[p].shares)}}{{=it.stats.pools[pool].payments[p].paid}} {{=it.stats.pools[pool].symbol}}
+ +{{ } }} diff --git a/website/pages/stats.html b/website/pages/stats.html new file mode 100644 index 0000000..30ed678 --- /dev/null +++ b/website/pages/stats.html @@ -0,0 +1,350 @@ + + +
+
+
Pool Historical Hashrate
+
+
+
+{{ function capitalizeFirstLetter(t){return t.charAt(0).toUpperCase()+t.slice(1)} }} +{{ function readableDate(a){ return new Date(parseInt(a)).toString(); } }} +
+ {{ for(var pool in it.stats.pools) { }} +
+
+
{{=capitalizeFirstLetter(it.stats.pools[pool].name)}} Pool Stats
+
+
+
{{=it.stats.pools[pool].minerCount}} Miners
+
{{=it.stats.pools[pool].workerCount}} Workers
+
{{=it.stats.pools[pool].hashrateString}} (Now)
+
... (Avg)
+
Luck {{=it.stats.pools[pool].luckDays}} Days
+
+
+
+
+
+
+
{{=capitalizeFirstLetter(it.stats.pools[pool].name)}} Network Stats
+
+
+
Block Height: {{=it.stats.pools[pool].poolStats.networkBlocks}}
+
Network Hash/s: {{=it.stats.pools[pool].poolStats.networkSolsString}}
+
Difficulty: {{=it.stats.pools[pool].poolStats.networkDiff}}
+
Node Connections: {{=it.stats.pools[pool].poolStats.networkConnections}}
+
+
+
+
+ {{ } }} +
+{{ for(var pool in it.stats.pools) { }} +{{ var blockscomb = new Array; }} +
+
+
+
{{=capitalizeFirstLetter(it.stats.pools[pool].name)}} Blocks Found    + + {{=it.stats.pools[pool].poolStats.validBlocks}} Blocks    + Paid: {{=(parseFloat(it.stats.pools[pool].poolStats.totalPaid)).toFixed(8)}} {{=it.stats.pools[pool].symbol}}   +
+
+ + + {{ for(var b in it.stats.pools[pool].pending.blocks) { }} + {{ var block = it.stats.pools[pool].pending.blocks[b].split(":"); }} +
+ Block: + {{if (String(it.stats.pools[pool].name).startsWith("zcash")) { }} + {{=block[2]}} + {{ } else if (String(it.stats.pools[pool].name).startsWith("zclassic")) { }} + {{=block[2]}} + {{ } else if (String(it.stats.pools[pool].name).startsWith("hush")) { }} + {{=block[2]}} + {{ } else { }} + {{=block[2]}} + {{ } }} + {{if (block[4] != null) { }} + {{=readableDate(block[4])}} + {{ } }} + {{if (it.stats.pools[pool].pending.confirms) { }} + {{if (it.stats.pools[pool].pending.confirms[block[0]]) { }} + {{=it.stats.pools[pool].pending.confirms[block[0]]}} of 100 + {{ } else { }} + *PENDING* + {{ } }} + {{ } else { }} + *PENDING* + {{ } }} +
Mined By: {{=block[3]}}
+
+ {{ blockscomb.push(block);}} + {{ } }} + {{ var i=0; for(var b in it.stats.pools[pool].confirmed.blocks) { }} + {{ if (i < 8) { i++; }} + {{ var block = it.stats.pools[pool].confirmed.blocks[b].split(":"); }} +
+ Block: + {{if (String(it.stats.pools[pool].name).startsWith("zcash")) { }} + {{=block[2]}} + {{ } else if (String(it.stats.pools[pool].name).startsWith("zclassic")) { }} + {{=block[2]}} + {{ } else { }} + {{=block[2]}} + {{ } }} + {{if (block[4] != null) { }} + {{=readableDate(block[4])}} + {{ } }} + *PAID* +
Mined By: {{=block[3]}}
+
+ {{blockscomb.push(block);}} + {{ } }} + {{ } }} + + +
+
+
+
+ + +
+
+
Finders of the last {{=blockscomb.length}} blocks
+ +
+
+
+ + +{{ } }} + + + + + diff --git a/website/pages/tbs.html b/website/pages/tbs.html new file mode 100644 index 0000000..09a5d27 --- /dev/null +++ b/website/pages/tbs.html @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + {{ for(var pool in it.stats.pools) { }} + + + + + + + + + + + + + {{ } }} +
PoolAlgoWorkersValid SharesInvalid SharesTotal BlocksPendingConfirmedOrphanedHashrate
{{=it.stats.pools[pool].name}}{{=it.stats.pools[pool].algorithm}}{{=Object.keys(it.stats.pools[pool].workers).length}}{{=it.stats.pools[pool].poolStats.validShares}}{{=it.stats.pools[pool].poolStats.invalidShares}}{{=it.stats.pools[pool].poolStats.validBlocks}}{{=it.stats.pools[pool].blocks.pending}}{{=it.stats.pools[pool].blocks.confirmed}}{{=it.stats.pools[pool].blocks.orphaned}}{{=it.stats.pools[pool].hashrateString}}
diff --git a/website/pages/workers.html b/website/pages/workers.html new file mode 100644 index 0000000..27f6378 --- /dev/null +++ b/website/pages/workers.html @@ -0,0 +1,94 @@ + + +{{ function capitalizeFirstLetter(t){return t.charAt(0).toUpperCase()+t.slice(1)} }} +{{ var i=0; for(var pool in it.stats.pools) { }} +
+
+
+ + Miner Lookup: + + + + + + + {{=capitalizeFirstLetter(it.stats.pools[pool].name)}} Top Miners    + {{=it.stats.pools[pool].minerCount}} Miners    + {{=it.stats.pools[pool].workerCount}} Workers    + {{=it.stats.pools[pool].shareCount}} Shares +
+
+ + + + + + + + + + {{ for(var worker in it.stats.pools[pool].miners) { }} + {{var workerstat = it.stats.pools[pool].miners[worker];}} + + + + + + + {{ } }} +
AddressSharesEfficiencyHashrate
{{=worker}}{{=Math.round(workerstat.currRoundShares * 100) / 100}}{{? workerstat.shares > 0}} {{=Math.floor(10000 * workerstat.shares / (workerstat.shares + workerstat.invalidshares)) / 100}}% {{??}} 0% {{?}}{{=workerstat.hashrateString}}
+
+
+
+{{ } }} diff --git a/website/static/admin.js b/website/static/admin.js new file mode 100644 index 0000000..82c3666 --- /dev/null +++ b/website/static/admin.js @@ -0,0 +1,100 @@ +var docCookies = { + getItem: function (sKey) { + return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null; + }, + setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) { + if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return false; } + var sExpires = ""; + if (vEnd) { + switch (vEnd.constructor) { + case Number: + sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd; + break; + case String: + sExpires = "; expires=" + vEnd; + break; + case Date: + sExpires = "; expires=" + vEnd.toUTCString(); + break; + } + } + document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : ""); + return true; + }, + removeItem: function (sKey, sPath, sDomain) { + if (!sKey || !this.hasItem(sKey)) { return false; } + document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ( sDomain ? "; domain=" + sDomain : "") + ( sPath ? "; path=" + sPath : ""); + return true; + }, + hasItem: function (sKey) { + return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie); + } +}; + +var password = docCookies.getItem('password'); + + +function showLogin(){ + $('#adminCenter').hide(); + $('#passwordForm').show(); +} + +function showAdminCenter(){ + $('#passwordForm').hide(); + $('#adminCenter').show(); +} + +function tryLogin(){ + apiRequest('pools', {}, function(response){ + showAdminCenter(); + displayMenu(response.result) + }); +} + +function displayMenu(pools){ + $('#poolList').after(Object.keys(pools).map(function(poolName){ + return '
  • ' + poolName + '
  • '; + }).join('')); +} + +function apiRequest(func, data, callback){ + var httpRequest = new XMLHttpRequest(); + httpRequest.onreadystatechange = function(){ + if (httpRequest.readyState === 4 && httpRequest.responseText){ + if (httpRequest.status === 401){ + docCookies.removeItem('password'); + $('#password').val(''); + showLogin(); + alert('Incorrect Password'); + } + else{ + var response = JSON.parse(httpRequest.responseText); + callback(response); + } + } + }; + httpRequest.open('POST', '/api/admin/' + func); + data.password = password; + httpRequest.setRequestHeader('Content-Type', 'application/json'); + httpRequest.send(JSON.stringify(data)); +} + +if (password){ + tryLogin(); +} +else{ + showLogin(); +} + +$('#passwordForm').submit(function(event){ + event.preventDefault(); + password = $('#password').val(); + if (password){ + if ($('#remember').is(':checked')) + docCookies.setItem('password', password, Infinity); + else + docCookies.setItem('password', password); + tryLogin(); + } + return false; +}); diff --git a/website/static/favicon.png b/website/static/favicon.png new file mode 100644 index 0000000..738dbba Binary files /dev/null and b/website/static/favicon.png differ diff --git a/website/static/logo.svg b/website/static/logo.svg new file mode 100644 index 0000000..0e92037 --- /dev/null +++ b/website/static/logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/static/main.js b/website/static/main.js new file mode 100644 index 0000000..3f8a05c --- /dev/null +++ b/website/static/main.js @@ -0,0 +1,30 @@ +$(function(){ + + var hotSwap = function(page, pushSate){ + if (pushSate) history.pushState(null, null, '/' + page); + $('.pure-menu-selected').removeClass('pure-menu-selected'); + $('a[href="/' + page + '"]').parent().addClass('pure-menu-selected'); + $.get("/get_page", {id: page}, function(data){ + $('main').html(data); + }, 'html') + }; + + $('.hot-swapper').click(function(event){ + if (event.which !== 1) return; + var pageId = $(this).attr('href').slice(1); + hotSwap(pageId, true); + event.preventDefault(); + return false; + }); + + window.addEventListener('load', function() { + setTimeout(function() { + window.addEventListener("popstate", function(e) { + hotSwap(location.pathname.slice(1)); + }); + }, 0); + }); + + window.statsSource = new EventSource("/api/live_stats"); + +}); diff --git a/website/static/miner_stats.js b/website/static/miner_stats.js new file mode 100644 index 0000000..36b53c3 --- /dev/null +++ b/website/static/miner_stats.js @@ -0,0 +1,246 @@ +var workerHashrateData; +var workerHashrateChart; +var workerHistoryMax = 160; + +var statData; +var totalHash; +var totalImmature; +var totalBal; +var totalPaid; +var totalShares; + +function getReadableHashRateString(hashrate){ + hashrate = (hashrate * 2); + if (hashrate < 1000000) { + return (Math.round(hashrate / 1000) / 1000 ).toFixed(2)+' Sol/s'; + } + var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ]; + var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1); + hashrate = (hashrate/1000) / Math.pow(1000, i + 1); + return hashrate.toFixed(2) + byteUnits[i]; +} + +function timeOfDayFormat(timestamp){ + var dStr = d3.time.format('%I:%M %p')(new Date(timestamp)); + if (dStr.indexOf('0') === 0) dStr = dStr.slice(1); + return dStr; +} + +function getWorkerNameFromAddress(w) { + var worker = w; + if (w.split(".").length > 1) { + worker = w.split(".")[1]; + if (worker == null || worker.length < 1) { + worker = "noname"; + } + } else { + worker = "noname"; + } + return worker; +} + +function buildChartData(){ + var workers = {}; + for (var w in statData.history) { + var worker = getWorkerNameFromAddress(w); + var a = workers[worker] = (workers[worker] || { + hashrate: [] + }); + for (var wh in statData.history[w]) { + a.hashrate.push([statData.history[w][wh].time * 1000, statData.history[w][wh].hashrate]); + } + if (a.hashrate.length > workerHistoryMax) { + workerHistoryMax = a.hashrate.length; + } + } + + var i=0; + workerHashrateData = []; + for (var worker in workers){ + workerHashrateData.push({ + key: worker, + disabled: (i > Math.min((_workerCount-1), 3)), + values: workers[worker].hashrate + }); + i++; + } +} + +function updateChartData(){ + var workers = {}; + for (var w in statData.history) { + var worker = getWorkerNameFromAddress(w); + // get a reference to lastest workerhistory + for (var wh in statData.history[w]) { } + //var wh = statData.history[w][statData.history[w].length - 1]; + var foundWorker = false; + for (var i = 0; i < workerHashrateData.length; i++) { + if (workerHashrateData[i].key === worker) { + foundWorker = true; + if (workerHashrateData[i].values.length >= workerHistoryMax) { + workerHashrateData[i].values.shift(); + } + workerHashrateData[i].values.push([statData.history[w][wh].time * 1000, statData.history[w][wh].hashrate]); + break; + } + } + if (!foundWorker) { + var hashrate = []; + hashrate.push([statData.history[w][wh].time * 1000, statData.history[w][wh].hashrate]); + workerHashrateData.push({ + key: worker, + values: hashrate + }); + rebuildWorkerDisplay(); + return true; + } + } + triggerChartUpdates(); + return false; +} + +function calculateAverageHashrate(worker) { + var count = 0; + var total = 1; + var avg = 0; + for (var i = 0; i < workerHashrateData.length; i++) { + count = 0; + for (var ii = 0; ii < workerHashrateData[i].values.length; ii++) { + if (worker == null || workerHashrateData[i].key === worker) { + count++; + avg += parseFloat(workerHashrateData[i].values[ii][1]); + } + } + if (count > total) + total = count; + } + avg = avg / total; + return avg; +} + +function triggerChartUpdates(){ + workerHashrateChart.update(); +} + +function displayCharts() { + nv.addGraph(function() { + workerHashrateChart = nv.models.lineChart() + .margin({left: 80, right: 30}) + .x(function(d){ return d[0] }) + .y(function(d){ return d[1] }) + .useInteractiveGuideline(true); + + workerHashrateChart.xAxis.tickFormat(timeOfDayFormat); + + workerHashrateChart.yAxis.tickFormat(function(d){ + return getReadableHashRateString(d); + }); + d3.select('#workerHashrate').datum(workerHashrateData).call(workerHashrateChart); + return workerHashrateChart; + }); +} + +function updateStats() { + totalHash = statData.totalHash; + totalPaid = statData.paid; + totalBal = statData.balance; + totalImmature = statData.immature; + totalShares = statData.totalShares; + // do some calculations + var _blocktime = 250; + var _networkHashRate = parseFloat(statData.networkSols) * 1.2; + var _myHashRate = (totalHash / 1000000) * 2; + var luckDays = ((_networkHashRate / _myHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3); + // update miner stats + $("#statsHashrate").text(getReadableHashRateString(totalHash)); + $("#statsHashrateAvg").text(getReadableHashRateString(calculateAverageHashrate(null))); + $("#statsLuckDays").text(luckDays); + $("#statsTotalImmature").text(totalImmature); + $("#statsTotalBal").text(totalBal); + $("#statsTotalPaid").text(totalPaid); + $("#statsTotalShares").text(totalShares.toFixed(2)); +} +function updateWorkerStats() { + // update worker stats + var i=0; + for (var w in statData.workers) { i++; + var htmlSafeWorkerName = w.split('.').join('_').replace(/[^\w\s]/gi, ''); + var saneWorkerName = getWorkerNameFromAddress(w); + $("#statsHashrate"+htmlSafeWorkerName).text(getReadableHashRateString(statData.workers[w].hashrate)); + $("#statsHashrateAvg"+htmlSafeWorkerName).text(getReadableHashRateString(calculateAverageHashrate(saneWorkerName))); + $("#statsLuckDays"+htmlSafeWorkerName).text(statData.workers[w].luckDays); + $("#statsPaid"+htmlSafeWorkerName).text(statData.workers[w].paid); + $("#statsBalance"+htmlSafeWorkerName).text(statData.workers[w].balance); + $("#statsShares"+htmlSafeWorkerName).text(Math.round(statData.workers[w].currRoundShares * 100) / 100); + $("#statsDiff"+htmlSafeWorkerName).text(statData.workers[w].diff); + } +} +function addWorkerToDisplay(name, htmlSafeName, workerObj) { + var htmlToAdd = ""; + htmlToAdd = '
    '; + if (htmlSafeName.indexOf("_") >= 0) { + htmlToAdd+= '
    '+htmlSafeName.substr(htmlSafeName.indexOf("_")+1,htmlSafeName.length)+'
    '; + } else { + htmlToAdd+= '
    noname
    '; + } + htmlToAdd+='
    '+getReadableHashRateString(workerObj.hashrate)+' (Now)
    '; + htmlToAdd+='
    '+getReadableHashRateString(calculateAverageHashrate(name))+' (Avg)
    '; + htmlToAdd+='
    Diff: '+workerObj.diff+'
    '; + htmlToAdd+='
    Shares: '+(Math.round(workerObj.currRoundShares * 100) / 100)+'
    '; + htmlToAdd+='
    Luck '+workerObj.luckDays+' Days
    '; + htmlToAdd+='
    Bal: '+workerObj.balance+'
    '; + htmlToAdd+='
    Paid: '+workerObj.paid+'
    '; + htmlToAdd+='
    '; + $("#boxesWorkers").html($("#boxesWorkers").html()+htmlToAdd); +} + +function rebuildWorkerDisplay() { + $("#boxesWorkers").html(""); + var i=0; + for (var w in statData.workers) { i++; + var htmlSafeWorkerName = w.split('.').join('_').replace(/[^\w\s]/gi, ''); + var saneWorkerName = getWorkerNameFromAddress(w); + addWorkerToDisplay(saneWorkerName, htmlSafeWorkerName, statData.workers[w]); + } +} + +// resize chart on window resize +nv.utils.windowResize(triggerChartUpdates); + +// grab initial stats +$.getJSON('/api/worker_stats?'+_miner, function(data){ + statData = data; + for (var w in statData.workers) { _workerCount++; } + buildChartData(); + displayCharts(); + rebuildWorkerDisplay(); + updateStats(); +}); + +// live stat updates +statsSource.addEventListener('message', function(e){ + // TODO, create miner_live_stats... + // miner_live_stats will return the same josn except without the worker history + // FOR NOW, use this to grab updated stats + $.getJSON('/api/worker_stats?'+_miner, function(data){ + statData = data; + // check for missing workers + var wc = 0; + var rebuilt = false; + // update worker stats + for (var w in statData.workers) { wc++; } + // TODO, this isn't 100% fool proof! + if (_workerCount != wc) { + if (_workerCount > wc) { + rebuildWorkerDisplay(); + rebuilt = true; + } + _workerCount = wc; + } + rebuilt = (rebuilt || updateChartData()); + updateStats(); + if (!rebuilt) { + updateWorkerStats(); + } + }); +}); diff --git a/website/static/nvd3.css b/website/static/nvd3.css new file mode 100644 index 0000000..d46f7eb --- /dev/null +++ b/website/static/nvd3.css @@ -0,0 +1 @@ +.chartWrap{margin:0;padding:0;overflow:hidden}.nvtooltip.with-3d-shadow,.with-3d-shadow .nvtooltip{-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nvtooltip{position:absolute;background-color:rgba(255,255,255,1);padding:1px;border:1px solid rgba(0,0,0,.2);z-index:10000;font-family:Arial;font-size:13px;text-align:left;pointer-events:none;white-space:nowrap;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.nvtooltip.with-transitions,.with-transitions .nvtooltip{transition:opacity 250ms linear;-moz-transition:opacity 250ms linear;-webkit-transition:opacity 250ms linear;transition-delay:250ms;-moz-transition-delay:250ms;-webkit-transition-delay:250ms}.nvtooltip.x-nvtooltip,.nvtooltip.y-nvtooltip{padding:8px}.nvtooltip h3{margin:0;padding:4px 14px;line-height:18px;font-weight:400;background-color:rgba(247,247,247,.75);text-align:center;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.nvtooltip p{margin:0;padding:5px 14px;text-align:center}.nvtooltip span{display:inline-block;margin:2px 0}.nvtooltip table{margin:6px;border-spacing:0}.nvtooltip table td{padding:2px 9px 2px 0;vertical-align:middle}.nvtooltip table td.key{font-weight:400}.nvtooltip table td.value{text-align:right;font-weight:700}.nvtooltip table tr.highlight td{padding:1px 9px 1px 0;border-bottom-style:solid;border-bottom-width:1px;border-top-style:solid;border-top-width:1px}.nvtooltip table td.legend-color-guide div{width:8px;height:8px;vertical-align:middle}.nvtooltip .footer{padding:3px;text-align:center}.nvtooltip-pending-removal{position:absolute;pointer-events:none}svg{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;display:block;width:100%;height:100%}svg text{font:400 12px Arial}svg .title{font:700 14px Arial}.nvd3 .nv-background{fill:#fff;fill-opacity:0}.nvd3.nv-noData{font-size:18px;font-weight:700}.nv-brush .extent{fill-opacity:.125;shape-rendering:crispEdges}.nvd3 .nv-legend .nv-series{cursor:pointer}.nvd3 .nv-legend .disabled circle{fill-opacity:0}.nvd3 .nv-axis{pointer-events:none}.nvd3 .nv-axis path{fill:none;stroke:#000;stroke-opacity:.75;shape-rendering:crispEdges}.nvd3 .nv-axis path.domain{stroke-opacity:.75}.nvd3 .nv-axis.nv-x path.domain{stroke-opacity:0}.nvd3 .nv-axis line{fill:none;stroke:#e5e5e5;shape-rendering:crispEdges}.nvd3 .nv-axis .zero line,.nvd3 .nv-axis line.zero{stroke-opacity:.75}.nvd3 .nv-axis .nv-axisMaxMin text{font-weight:700}.nvd3 .x .nv-axis .nv-axisMaxMin text,.nvd3 .x2 .nv-axis .nv-axisMaxMin text,.nvd3 .x3 .nv-axis .nv-axisMaxMin text{text-anchor:middle}.nv-brush .resize path{fill:#eee;stroke:#666}.nvd3 .nv-bars .negative rect{zfill:brown}.nvd3 .nv-bars rect{zfill:#4682b4;fill-opacity:.75;transition:fill-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear}.nvd3 .nv-bars rect.hover{fill-opacity:1}.nvd3 .nv-bars .hover rect{fill:#add8e6}.nvd3 .nv-bars text{fill:rgba(0,0,0,0)}.nvd3 .nv-bars .hover text{fill:rgba(0,0,0,1)}.nvd3 .nv-multibar .nv-groups rect,.nvd3 .nv-multibarHorizontal .nv-groups rect,.nvd3 .nv-discretebar .nv-groups rect{stroke-opacity:0;transition:fill-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear}.nvd3 .nv-multibar .nv-groups rect:hover,.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,.nvd3 .nv-discretebar .nv-groups rect:hover{fill-opacity:1}.nvd3 .nv-discretebar .nv-groups text,.nvd3 .nv-multibarHorizontal .nv-groups text{font-weight:700;fill:rgba(0,0,0,1);stroke:rgba(0,0,0,0)}.nvd3.nv-pie path{stroke-opacity:0;transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-pie .nv-slice text{stroke:#000;stroke-width:0}.nvd3.nv-pie path{stroke:#fff;stroke-width:1px;stroke-opacity:1}.nvd3.nv-pie .hover path{fill-opacity:.7}.nvd3.nv-pie .nv-label{pointer-events:none}.nvd3.nv-pie .nv-label rect{fill-opacity:0;stroke-opacity:0}.nvd3 .nv-groups path.nv-line{fill:none;stroke-width:1.5px}.nvd3 .nv-groups path.nv-line.nv-thin-line{stroke-width:1px}.nvd3 .nv-groups path.nv-area{stroke:none}.nvd3 .nv-line.hover path{stroke-width:6px}.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point{fill-opacity:0;stroke-opacity:0}.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point{fill-opacity:.5!important;stroke-opacity:.5!important}.with-transitions .nvd3 .nv-groups .nv-point{transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-moz-transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-webkit-transition:stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-scatter .nv-groups .nv-point.hover,.nvd3 .nv-groups .nv-point.hover{stroke-width:7px;fill-opacity:.95!important;stroke-opacity:.95!important}.nvd3 .nv-point-paths path{stroke:#aaa;stroke-opacity:0;fill:#eee;fill-opacity:0}.nvd3 .nv-indexLine{cursor:ew-resize}.nvd3 .nv-distribution{pointer-events:none}.nvd3 .nv-groups .nv-point.hover{stroke-width:20px;stroke-opacity:.5}.nvd3 .nv-scatter .nv-point.hover{fill-opacity:1}.nvd3.nv-stackedarea path.nv-area{fill-opacity:.7;stroke-opacity:0;transition:fill-opacity 250ms linear,stroke-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear,stroke-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-stackedarea path.nv-area.hover{fill-opacity:.9}.nvd3.nv-stackedarea .nv-groups .nv-point{stroke-opacity:0;fill-opacity:0}.nvd3.nv-linePlusBar .nv-bar rect{fill-opacity:.75}.nvd3.nv-linePlusBar .nv-bar rect:hover{fill-opacity:1}.nvd3.nv-bullet{font:10px sans-serif}.nvd3.nv-bullet .nv-measure{fill-opacity:.8}.nvd3.nv-bullet .nv-measure:hover{fill-opacity:1}.nvd3.nv-bullet .nv-marker{stroke:#000;stroke-width:2px}.nvd3.nv-bullet .nv-markerTriangle{stroke:#000;fill:#fff;stroke-width:1.5px}.nvd3.nv-bullet .nv-tick line{stroke:#666;stroke-width:.5px}.nvd3.nv-bullet .nv-range.nv-s0{fill:#eee}.nvd3.nv-bullet .nv-range.nv-s1{fill:#ddd}.nvd3.nv-bullet .nv-range.nv-s2{fill:#ccc}.nvd3.nv-bullet .nv-title{font-size:14px;font-weight:700}.nvd3.nv-bullet .nv-subtitle{fill:#999}.nvd3.nv-bullet .nv-range{fill:#bababa;fill-opacity:.4}.nvd3.nv-bullet .nv-range:hover{fill-opacity:.7}.nvd3.nv-sparkline path{fill:none}.nvd3.nv-sparklineplus g.nv-hoverValue{pointer-events:none}.nvd3.nv-sparklineplus .nv-hoverValue line{stroke:#333;stroke-width:1.5px}.nvd3.nv-sparklineplus,.nvd3.nv-sparklineplus g{pointer-events:all}.nvd3 .nv-hoverArea{fill-opacity:0;stroke-opacity:0}.nvd3.nv-sparklineplus .nv-xValue,.nvd3.nv-sparklineplus .nv-yValue{stroke-width:0;font-size:.9em;font-weight:400}.nvd3.nv-sparklineplus .nv-yValue{stroke:#f66}.nvd3.nv-sparklineplus .nv-maxValue{stroke:#2ca02c;fill:#2ca02c}.nvd3.nv-sparklineplus .nv-minValue{stroke:#d62728;fill:#d62728}.nvd3.nv-sparklineplus .nv-currentValue{font-weight:700;font-size:1.1em}.nvd3.nv-ohlcBar .nv-ticks .nv-tick{stroke-width:2px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover{stroke-width:4px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive{stroke:#2ca02c}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative{stroke:#d62728}.nvd3.nv-historicalStockChart .nv-axis .nv-axislabel{font-weight:700}.nvd3.nv-historicalStockChart .nv-dragTarget{fill-opacity:0;stroke:none;cursor:move}.nvd3 .nv-brush .extent{fill-opacity:0!important}.nvd3 .nv-brushBackground rect{stroke:#000;stroke-width:.4;fill:#fff;fill-opacity:.7}.nvd3.nv-indentedtree .name{margin-left:5px}.nvd3.nv-indentedtree .clickable{color:#08C;cursor:pointer}.nvd3.nv-indentedtree span.clickable:hover{color:#005580;text-decoration:underline}.nvd3.nv-indentedtree .nv-childrenCount{display:inline-block;margin-left:5px}.nvd3.nv-indentedtree .nv-treeicon{cursor:pointer}.nvd3.nv-indentedtree .nv-treeicon.nv-folded{cursor:pointer}.nvd3 .background path{fill:none;stroke:#ccc;stroke-opacity:.4;shape-rendering:crispEdges}.nvd3 .foreground path{fill:none;stroke:#4682b4;stroke-opacity:.7}.nvd3 .brush .extent{fill-opacity:.3;stroke:#fff;shape-rendering:crispEdges}.nvd3 .axis line,.axis path{fill:none;stroke:#000;shape-rendering:crispEdges}.nvd3 .axis text{text-shadow:0 1px 0 #fff}.nvd3 .nv-interactiveGuideLine{pointer-events:none}.nvd3 line.nv-guideline{stroke:#ccc} \ No newline at end of file diff --git a/website/static/nvd3.js b/website/static/nvd3.js new file mode 100644 index 0000000..bddd4ae --- /dev/null +++ b/website/static/nvd3.js @@ -0,0 +1,6 @@ +(function(){function t(e,t){return(new Date(t,e+1,0)).getDate()}function n(e,t,n){return function(r,i,s){var o=e(r),u=[];o1)while(op||r>d||d3.event.relatedTarget&&d3.event.relatedTarget.ownerSVGElement===undefined||a){if(l&&d3.event.relatedTarget&&d3.event.relatedTarget.ownerSVGElement===undefined&&d3.event.relatedTarget.className.match(t.nvPointerEventsClass))return;u.elementMouseout({mouseX:n,mouseY:r}),c.renderGuideLine(null);return}var f=s.invert(n);u.elementMousemove({mouseX:n,mouseY:r,pointXValue:f}),d3.event.type==="dblclick"&&u.elementDblclick({mouseX:n,mouseY:r,pointXValue:f})}var h=d3.select(this),p=n||960,d=r||400,v=h.selectAll("g.nv-wrap.nv-interactiveLineLayer").data([o]),m=v.enter().append("g").attr("class"," nv-wrap nv-interactiveLineLayer");m.append("g").attr("class","nv-interactiveGuideLine");if(!f)return;f.on("mousemove",g,!0).on("mouseout",g,!0).on("dblclick",g),c.renderGuideLine=function(t){if(!a)return;var n=v.select(".nv-interactiveGuideLine").selectAll("line").data(t!=null?[e.utils.NaNtoZero(t)]:[],String);n.enter().append("line").attr("class","nv-guideline").attr("x1",function(e){return e}).attr("x2",function(e){return e}).attr("y1",d).attr("y2",0),n.exit().remove()}})}var t=e.models.tooltip(),n=null,r=null,i={left:0,top:0},s=d3.scale.linear(),o=d3.scale.linear(),u=d3.dispatch("elementMousemove","elementMouseout","elementDblclick"),a=!0,f=null,l=navigator.userAgent.indexOf("MSIE")!==-1;return c.dispatch=u,c.tooltip=t,c.margin=function(e){return arguments.length?(i.top=typeof e.top!="undefined"?e.top:i.top,i.left=typeof e.left!="undefined"?e.left:i.left,c):i},c.width=function(e){return arguments.length?(n=e,c):n},c.height=function(e){return arguments.length?(r=e,c):r},c.xScale=function(e){return arguments.length?(s=e,c):s},c.showGuideLine=function(e){return arguments.length?(a=e,c):a},c.svgContainer=function(e){return arguments.length?(f=e,c):f},c},e.interactiveBisect=function(e,t,n){"use strict";if(!e instanceof Array)return null;typeof n!="function"&&(n=function(e,t){return e.x});var r=d3.bisector(n).left,i=d3.max([0,r(e,t)-1]),s=n(e[i],i);typeof s=="undefined"&&(s=i);if(s===t)return i;var o=d3.min([i+1,e.length-1]),u=n(e[o],o);return typeof u=="undefined"&&(u=o),Math.abs(u-t)>=Math.abs(s-t)?i:o},e.nearestValueIndex=function(e,t,n){"use strict";var r=Infinity,i=null;return e.forEach(function(e,s){var o=Math.abs(t-e);o<=r&&oT.height?0:x}v.top=Math.abs(x-S.top),v.left=Math.abs(E.left-S.left)}t+=a.offsetLeft+v.left-2*a.scrollLeft,u+=a.offsetTop+v.top-2*a.scrollTop}return s&&s>0&&(u=Math.floor(u/s)*s),e.tooltip.calcTooltipPosition([t,u],r,i,h),w}var t=null,n=null,r="w",i=50,s=25,o=null,u=null,a=null,f=null,l={left:null,top:null},c=!0,h="nvtooltip-"+Math.floor(Math.random()*1e5),p="nv-pointer-events-none",d=function(e,t){return e},v=function(e){return e},m=function(e){if(t!=null)return t;if(e==null)return"";var n=d3.select(document.createElement("table")),r=n.selectAll("thead").data([e]).enter().append("thead");r.append("tr").append("td").attr("colspan",3).append("strong").classed("x-value",!0).html(v(e.value));var i=n.selectAll("tbody").data([e]).enter().append("tbody"),s=i.selectAll("tr").data(function(e){return e.series}).enter().append("tr").classed("highlight",function(e){return e.highlight});s.append("td").classed("legend-color-guide",!0).append("div").style("background-color",function(e){return e.color}),s.append("td").classed("key",!0).html(function(e){return e.key}),s.append("td").classed("value",!0).html(function(e,t){return d(e.value,t)}),s.selectAll("td").each(function(e){if(e.highlight){var t=d3.scale.linear().domain([0,1]).range(["#fff",e.color]),n=.6;d3.select(this).style("border-bottom-color",t(n)).style("border-top-color",t(n))}});var o=n.node().outerHTML;return e.footer!==undefined&&(o+=""),o},g=function(e){return e&&e.series&&e.series.length>0?!0:!1};return w.nvPointerEventsClass=p,w.content=function(e){return arguments.length?(t=e,w):t},w.tooltipElem=function(){return f},w.contentGenerator=function(e){return arguments.length?(typeof e=="function"&&(m=e),w):m},w.data=function(e){return arguments.length?(n=e,w):n},w.gravity=function(e){return arguments.length?(r=e,w):r},w.distance=function(e){return arguments.length?(i=e,w):i},w.snapDistance=function(e){return arguments.length?(s=e,w):s},w.classes=function(e){return arguments.length?(u=e,w):u},w.chartContainer=function(e){return arguments.length?(a=e,w):a},w.position=function(e){return arguments.length?(l.left=typeof e.left!="undefined"?e.left:l.left,l.top=typeof e.top!="undefined"?e.top:l.top,w):l},w.fixedTop=function(e){return arguments.length?(o=e,w):o},w.enabled=function(e){return arguments.length?(c=e,w):c},w.valueFormatter=function(e){return arguments.length?(typeof e=="function"&&(d=e),w):d},w.headerFormatter=function(e){return arguments.length?(typeof e=="function"&&(v=e),w):v},w.id=function(){return h},w},e.tooltip.show=function(t,n,r,i,s,o){var u=document.createElement("div");u.className="nvtooltip "+(o?o:"xy-tooltip");var a=s;if(!s||s.tagName.match(/g|svg/i))a=document.getElementsByTagName("body")[0];u.style.left=0,u.style.top=0,u.style.opacity=0,u.innerHTML=n,a.appendChild(u),s&&(t[0]=t[0]-s.scrollLeft,t[1]=t[1]-s.scrollTop),e.tooltip.calcTooltipPosition(t,r,i,u)},e.tooltip.findFirstNonSVGParent=function(e){while(e.tagName.match(/^g|svg$/i)!==null)e=e.parentNode;return e},e.tooltip.findTotalOffsetTop=function(e,t){var n=t;do isNaN(e.offsetTop)||(n+=e.offsetTop);while(e=e.offsetParent);return n},e.tooltip.findTotalOffsetLeft=function(e,t){var n=t;do isNaN(e.offsetLeft)||(n+=e.offsetLeft);while(e=e.offsetParent);return n},e.tooltip.calcTooltipPosition=function(t,n,r,i){var s=parseInt(i.offsetHeight),o=parseInt(i.offsetWidth),u=e.utils.windowSize().width,a=e.utils.windowSize().height,f=window.pageYOffset,l=window.pageXOffset,c,h;a=window.innerWidth>=document.body.scrollWidth?a:a-16,u=window.innerHeight>=document.body.scrollHeight?u:u-16,n=n||"s",r=r||20;var p=function(t){return e.tooltip.findTotalOffsetTop(t,h)},d=function(t){return e.tooltip.findTotalOffsetLeft(t,c)};switch(n){case"e":c=t[0]-o-r,h=t[1]-s/2;var v=d(i),m=p(i);vl?t[0]+r:l-v+c),mf+a&&(h=f+a-m+h-s);break;case"w":c=t[0]+r,h=t[1]-s/2;var v=d(i),m=p(i);v+o>u&&(c=t[0]-o-r),mf+a&&(h=f+a-m+h-s);break;case"n":c=t[0]-o/2-5,h=t[1]+r;var v=d(i),m=p(i);vu&&(c=c-o/2+5),m+s>f+a&&(h=f+a-m+h-s);break;case"s":c=t[0]-o/2,h=t[1]-s-r;var v=d(i),m=p(i);vu&&(c=c-o/2+5),f>m&&(h=f);break;case"none":c=t[0],h=t[1]-r;var v=d(i),m=p(i)}return i.style.left=c+"px",i.style.top=h+"px",i.style.opacity=1,i.style.position="absolute",i},e.tooltip.cleanup=function(){var e=document.getElementsByClassName("nvtooltip"),t=[];while(e.length)t.push(e[0]),e[0].style.transitionDelay="0 !important",e[0].style.opacity=0,e[0].className="nvtooltip-pending-removal";setTimeout(function(){while(t.length){var e=t.pop();e.parentNode.removeChild(e)}},500)}}(),e.utils.windowSize=function(){var e={width:640,height:480};return document.body&&document.body.offsetWidth&&(e.width=document.body.offsetWidth,e.height=document.body.offsetHeight),document.compatMode=="CSS1Compat"&&document.documentElement&&document.documentElement.offsetWidth&&(e.width=document.documentElement.offsetWidth,e.height=document.documentElement.offsetHeight),window.innerWidth&&window.innerHeight&&(e.width=window.innerWidth,e.height=window.innerHeight),e},e.utils.windowResize=function(e){if(e===undefined)return;var t=window.onresize;window.onresize=function(n){typeof t=="function"&&t(n),e(n)}},e.utils.getColor=function(t){return arguments.length?Object.prototype.toString.call(t)==="[object Array]"?function(e,n){return e.color||t[n%t.length]}:t:e.utils.defaultColor()},e.utils.defaultColor=function(){var e=d3.scale.category20().range();return function(t,n){return t.color||e[n%e.length]}},e.utils.customTheme=function(e,t,n){t=t||function(e){return e.key},n=n||d3.scale.category20().range();var r=n.length;return function(i,s){var o=t(i);return r||(r=n.length),typeof e[o]!="undefined"?typeof e[o]=="function"?e[o]():e[o]:n[--r]}},e.utils.pjax=function(t,n){function r(r){d3.html(r,function(r){var i=d3.select(n).node();i.parentNode.replaceChild(d3.select(r).select(n).node(),i),e.utils.pjax(t,n)})}d3.selectAll(t).on("click",function(){history.pushState(this.href,this.textContent,this.href),r(this.href),d3.event.preventDefault()}),d3.select(window).on("popstate",function(){d3.event.state&&r(d3.event.state)})},e.utils.calcApproxTextWidth=function(e){if(typeof e.style=="function"&&typeof e.text=="function"){var t=parseInt(e.style("font-size").replace("px","")),n=e.text().length;return n*t*.5}return 0},e.utils.NaNtoZero=function(e){return typeof e!="number"||isNaN(e)||e===null||e===Infinity?0:e},e.utils.optionsFunc=function(e){return e&&d3.map(e).forEach(function(e,t){typeof this[e]=="function"&&this[e](t)}.bind(this)),this},e.models.axis=function(){"use strict";function m(e){return e.each(function(e){var i=d3.select(this),m=i.selectAll("g.nv-wrap.nv-axis").data([e]),g=m.enter().append("g").attr("class","nvd3 nv-wrap nv-axis"),y=g.append("g"),b=m.select("g");p!==null?t.ticks(p):(t.orient()=="top"||t.orient()=="bottom")&&t.ticks(Math.abs(s.range()[1]-s.range()[0])/100),b.transition().call(t),v=v||t.scale();var w=t.tickFormat();w==null&&(w=v.tickFormat());var E=b.selectAll("text.nv-axislabel").data([o||null]);E.exit().remove();switch(t.orient()){case"top":E.enter().append("text").attr("class","nv-axislabel");var S=s.range().length==2?s.range()[1]:s.range()[s.range().length-1]+(s.range()[1]-s.range()[0]);E.attr("text-anchor","middle").attr("y",0).attr("x",S/2);if(u){var x=m.selectAll("g.nv-axisMaxMin").data(s.domain());x.enter().append("g").attr("class","nv-axisMaxMin").append("text"),x.exit().remove(),x.attr("transform",function(e,t){return"translate("+s(e)+",0)"}).select("text").attr("dy","-0.5em").attr("y",-t.tickPadding()).attr("text-anchor","middle").text(function(e,t){var n=w(e);return(""+n).match("NaN")?"":n}),x.transition().attr("transform",function(e,t){return"translate("+s.range()[t]+",0)"})}break;case"bottom":var T=36,N=30,C=b.selectAll("g").select("text");if(f%360){C.each(function(e,t){var n=this.getBBox().width;n>N&&(N=n)});var k=Math.abs(Math.sin(f*Math.PI/180)),T=(k?k*N:N)+30;C.attr("transform",function(e,t,n){return"rotate("+f+" 0,0)"}).style("text-anchor",f%360>0?"start":"end")}E.enter().append("text").attr("class","nv-axislabel");var S=s.range().length==2?s.range()[1]:s.range()[s.range().length-1]+(s.range()[1]-s.range()[0]);E.attr("text-anchor","middle").attr("y",T).attr("x",S/2);if(u){var x=m.selectAll("g.nv-axisMaxMin").data([s.domain()[0],s.domain()[s.domain().length-1]]);x.enter().append("g").attr("class","nv-axisMaxMin").append("text"),x.exit().remove(),x.attr("transform",function(e,t){return"translate("+(s(e)+(h?s.rangeBand()/2:0))+",0)"}).select("text").attr("dy",".71em").attr("y",t.tickPadding()).attr("transform",function(e,t,n){return"rotate("+f+" 0,0)"}).style("text-anchor",f?f%360>0?"start":"end":"middle").text(function(e,t){var n=w(e);return(""+n).match("NaN")?"":n}),x.transition().attr("transform",function(e,t){return"translate("+(s(e)+(h?s.rangeBand()/2:0))+",0)"})}c&&C.attr("transform",function(e,t){return"translate(0,"+(t%2==0?"0":"12")+")"});break;case"right":E.enter().append("text").attr("class","nv-axislabel"),E.style("text-anchor",l?"middle":"begin").attr("transform",l?"rotate(90)":"").attr("y",l?-Math.max(n.right,r)+12:-10).attr("x",l?s.range()[0]/2:t.tickPadding());if(u){var x=m.selectAll("g.nv-axisMaxMin").data(s.domain());x.enter().append("g").attr("class","nv-axisMaxMin").append("text").style("opacity",0),x.exit().remove(),x.attr("transform",function(e,t){return"translate(0,"+s(e)+")"}).select("text").attr("dy",".32em").attr("y",0).attr("x",t.tickPadding()).style("text-anchor","start").text(function(e,t){var n=w(e);return(""+n).match("NaN")?"":n}),x.transition().attr("transform",function(e,t){return"translate(0,"+s.range()[t]+")"}).select("text").style("opacity",1)}break;case"left":E.enter().append("text").attr("class","nv-axislabel"),E.style("text-anchor",l?"middle":"end").attr("transform",l?"rotate(-90)":"").attr("y",l?-Math.max(n.left,r)+d:-10).attr("x",l?-s.range()[0]/2:-t.tickPadding());if(u){var x=m.selectAll("g.nv-axisMaxMin").data(s.domain());x.enter().append("g").attr("class","nv-axisMaxMin").append("text").style("opacity",0),x.exit().remove(),x.attr("transform",function(e,t){return"translate(0,"+v(e)+")"}).select("text").attr("dy",".32em").attr("y",0).attr("x",-t.tickPadding()).attr("text-anchor","end").text(function(e,t){var n=w(e);return(""+n).match("NaN")?"":n}),x.transition().attr("transform",function(e,t){return"translate(0,"+s.range()[t]+")"}).select("text").style("opacity",1)}}E.text(function(e){return e}),u&&(t.orient()==="left"||t.orient()==="right")&&(b.selectAll("g").each(function(e,t){d3.select(this).select("text").attr("opacity",1);if(s(e)s.range()[0]-10)(e>1e-10||e<-1e-10)&&d3.select(this).attr("opacity",0),d3.select(this).select("text").attr("opacity",0)}),s.domain()[0]==s.domain()[1]&&s.domain()[0]==0&&m.selectAll("g.nv-axisMaxMin").style("opacity",function(e,t){return t?0:1}));if(u&&(t.orient()==="top"||t.orient()==="bottom")){var L=[];m.selectAll("g.nv-axisMaxMin").each(function(e,t){try{t?L.push(s(e)-this.getBBox().width-4):L.push(s(e)+this.getBBox().width+4)}catch(n){t?L.push(s(e)-4):L.push(s(e)+4)}}),b.selectAll("g").each(function(e,t){if(s(e)L[1])e>1e-10||e<-1e-10?d3.select(this).remove():d3.select(this).select("text").remove()})}a&&b.selectAll(".tick").filter(function(e){return!parseFloat(Math.round(e.__data__*1e5)/1e6)&&e.__data__!==undefined}).classed("zero",!0),v=s.copy()}),m}var t=d3.svg.axis(),n={top:0,right:0,bottom:0,left:0},r=75,i=60,s=d3.scale.linear(),o=null,u=!0,a=!0,f=0,l=!0,c=!1,h=!1,p=null,d=12;t.scale(s).orient("bottom").tickFormat(function(e){return e});var v;return m.axis=t,d3.rebind(m,t,"orient","tickValues","tickSubdivide","tickSize","tickPadding","tickFormat"),d3.rebind(m,s,"domain","range","rangeBand","rangeBands"),m.options=e.utils.optionsFunc.bind(m),m.margin=function(e){return arguments.length?(n.top=typeof e.top!="undefined"?e.top:n.top,n.right=typeof e.right!="undefined"?e.right:n.right,n.bottom=typeof e.bottom!="undefined"?e.bottom:n.bottom,n.left=typeof e.left!="undefined"?e.left:n.left,m):n},m.width=function(e){return arguments.length?(r=e,m):r},m.ticks=function(e){return arguments.length?(p=e,m):p},m.height=function(e){return arguments.length?(i=e,m):i},m.axisLabel=function(e){return arguments.length?(o=e,m):o},m.showMaxMin=function(e){return arguments.length?(u=e,m):u},m.highlightZero=function(e){return arguments.length?(a=e,m):a},m.scale=function(e){return arguments.length?(s=e,t.scale(s),h=typeof s.rangeBands=="function",d3.rebind(m,s,"domain","range","rangeBand","rangeBands"),m):s},m.rotateYLabel=function(e){return arguments.length?(l=e,m):l},m.rotateLabels=function(e){return arguments.length?(f=e,m):f},m.staggerLabels=function(e){return arguments.length?(c=e,m):c},m.axisLabelDistance=function(e){return arguments.length?(d=e,m):d},m},e.models.bullet=function(){"use strict";function m(e){return e.each(function(e,n){var p=c-t.left-t.right,m=h-t.top-t.bottom,g=d3.select(this),y=i.call(this,e,n).slice().sort(d3.descending),b=s.call(this,e,n).slice().sort(d3.descending),w=o.call(this,e,n).slice().sort(d3.descending),E=u.call(this,e,n).slice(),S=a.call(this,e,n).slice(),x=f.call(this,e,n).slice(),T=d3.scale.linear().domain(d3.extent(d3.merge([l,y]))).range(r?[p,0]:[0,p]),N=this.__chart__||d3.scale.linear().domain([0,Infinity]).range(T.range());this.__chart__=T;var C=d3.min(y),k=d3.max(y),L=y[1],A=g.selectAll("g.nv-wrap.nv-bullet").data([e]),O=A.enter().append("g").attr("class","nvd3 nv-wrap nv-bullet"),M=O.append("g"),_=A.select("g");M.append("rect").attr("class","nv-range nv-rangeMax"),M.append("rect").attr("class","nv-range nv-rangeAvg"),M.append("rect").attr("class","nv-range nv-rangeMin"),M.append("rect").attr("class","nv-measure"),M.append("path").attr("class","nv-markerTriangle"),A.attr("transform","translate("+t.left+","+t.top+")");var D=function(e){return Math.abs(N(e)-N(0))},P=function(e){return Math.abs(T(e)-T(0))},H=function(e){return e<0?N(e):N(0)},B=function(e){return e<0?T(e):T(0)};_.select("rect.nv-rangeMax").attr("height",m).attr("width",P(k>0?k:C)).attr("x",B(k>0?k:C)).datum(k>0?k:C),_.select("rect.nv-rangeAvg").attr("height",m).attr("width",P(L)).attr("x",B(L)).datum(L),_.select("rect.nv-rangeMin").attr("height",m).attr("width",P(k)).attr("x",B(k)).attr("width",P(k>0?C:k)).attr("x",B(k>0?C:k)).datum(k>0?C:k),_.select("rect.nv-measure").style("fill",d).attr("height",m/3).attr("y",m/3).attr("width",w<0?T(0)-T(w[0]):T(w[0])-T(0)).attr("x",B(w)).on("mouseover",function(){v.elementMouseover({value:w[0],label:x[0]||"Current",pos:[T(w[0]),m/2]})}).on("mouseout",function(){v.elementMouseout({value:w[0],label:x[0]||"Current"})});var j=m/6;b[0]?_.selectAll("path.nv-markerTriangle").attr("transform",function(e){return"translate("+T(b[0])+","+m/2+")"}).attr("d","M0,"+j+"L"+j+","+ -j+" "+ -j+","+ -j+"Z").on("mouseover",function(){v.elementMouseover({value:b[0],label:S[0]||"Previous",pos:[T(b[0]),m/2]})}).on("mouseout",function(){v.elementMouseout({value:b[0],label:S[0]||"Previous"})}):_.selectAll("path.nv-markerTriangle").remove(),A.selectAll(".nv-range").on("mouseover",function(e,t){var n=E[t]||(t?t==1?"Mean":"Minimum":"Maximum");v.elementMouseover({value:e,label:n,pos:[T(e),m/2]})}).on("mouseout",function(e,t){var n=E[t]||(t?t==1?"Mean":"Minimum":"Maximum");v.elementMouseout({value:e,label:n})})}),m}var t={top:0,right:0,bottom:0,left:0},n="left",r=!1,i=function(e){return e.ranges},s=function(e){return e.markers},o=function(e){return e.measures},u=function(e){return e.rangeLabels?e.rangeLabels:[]},a=function(e){return e.markerLabels?e.markerLabels:[]},f=function(e){return e.measureLabels?e.measureLabels:[]},l=[0],c=380,h=30,p=null,d=e.utils.getColor(["#1f77b4"]),v=d3.dispatch("elementMouseover","elementMouseout");return m.dispatch=v,m.options=e.utils.optionsFunc.bind(m),m.orient=function(e){return arguments.length?(n=e,r=n=="right"||n=="bottom",m):n},m.ranges=function(e){return arguments.length?(i=e,m):i},m.markers=function(e){return arguments.length?(s=e,m):s},m.measures=function(e){return arguments.length?(o=e,m):o},m.forceX=function(e){return arguments.length?(l=e,m):l},m.width=function(e){return arguments.length?(c=e,m):c},m.height=function(e){return arguments.length?(h=e,m):h},m.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,m):t},m.tickFormat=function(e){return arguments.length?(p=e,m):p},m.color=function(t){return arguments.length?(d=e.utils.getColor(t),m):d},m},e.models.bulletChart=function(){"use strict";function m(e){return e.each(function(n,h){var g=d3.select(this),y=(a||parseInt(g.style("width"))||960)-i.left-i.right,b=f-i.top-i.bottom,w=this;m.update=function(){m(e)},m.container=this;if(!n||!s.call(this,n,h)){var E=g.selectAll(".nv-noData").data([p]);return E.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),E.attr("x",i.left+y/2).attr("y",18+i.top+b/2).text(function(e){return e}),m}g.selectAll(".nv-noData").remove();var S=s.call(this,n,h).slice().sort(d3.descending),x=o.call(this,n,h).slice().sort(d3.descending),T=u.call(this,n,h).slice().sort(d3.descending),N=g.selectAll("g.nv-wrap.nv-bulletChart").data([n]),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-bulletChart"),k=C.append("g"),L=N.select("g");k.append("g").attr("class","nv-bulletWrap"),k.append("g").attr("class","nv-titles"),N.attr("transform","translate("+i.left+","+i.top+")");var A=d3.scale.linear().domain([0,Math.max(S[0],x[0],T[0])]).range(r?[y,0]:[0,y]),O=this.__chart__||d3.scale.linear().domain([0,Infinity]).range(A.range());this.__chart__=A;var M=function(e){return Math.abs(O(e)-O(0))},_=function(e){return Math.abs(A(e)-A(0))},D=k.select(".nv-titles").append("g").attr("text-anchor","end").attr("transform","translate(-6,"+(f-i.top-i.bottom)/2+")");D.append("text").attr("class","nv-title").text(function(e){return e.title}),D.append("text").attr("class","nv-subtitle").attr("dy","1em").text(function(e){return e.subtitle}),t.width(y).height(b);var P=L.select(".nv-bulletWrap");d3.transition(P).call(t);var H=l||A.tickFormat(y/100),B=L.selectAll("g.nv-tick").data(A.ticks(y/50),function(e){return this.textContent||H(e)}),j=B.enter().append("g").attr("class","nv-tick").attr("transform",function(e){return"translate("+O(e)+",0)"}).style("opacity",1e-6);j.append("line").attr("y1",b).attr("y2",b*7/6),j.append("text").attr("text-anchor","middle").attr("dy","1em").attr("y",b*7/6).text(H);var F=d3.transition(B).attr("transform",function(e){return"translate("+A(e)+",0)"}).style("opacity",1);F.select("line").attr("y1",b).attr("y2",b*7/6),F.select("text").attr("y",b*7/6),d3.transition(B.exit()).attr("transform",function(e){return"translate("+A(e)+",0)"}).style("opacity",1e-6).remove(),d.on("tooltipShow",function(e){e.key=n.title,c&&v(e,w.parentNode)})}),d3.timer.flush(),m}var t=e.models.bullet(),n="left",r=!1,i={top:5,right:40,bottom:20,left:120},s=function(e){return e.ranges},o=function(e){return e.markers},u=function(e){return e.measures},a=null,f=55,l=null,c=!0,h=function(e,t,n,r,i){return"

    "+t+"

    "+"

    "+n+"

    "},p="No Data Available.",d=d3.dispatch("tooltipShow","tooltipHide"),v=function(t,n){var r=t.pos[0]+(n.offsetLeft||0)+i.left,s=t.pos[1]+(n.offsetTop||0)+i.top,o=h(t.key,t.label,t.value,t,m);e.tooltip.show([r,s],o,t.value<0?"e":"w",null,n)};return t.dispatch.on("elementMouseover.tooltip",function(e){d.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){d.tooltipHide(e)}),d.on("tooltipHide",function(){c&&e.tooltip.cleanup()}),m.dispatch=d,m.bullet=t,d3.rebind(m,t,"color"),m.options=e.utils.optionsFunc.bind(m),m.orient=function(e){return arguments.length?(n=e,r=n=="right"||n=="bottom",m):n},m.ranges=function(e){return arguments.length?(s=e,m):s},m.markers=function(e){return arguments.length?(o=e,m):o},m.measures=function(e){return arguments.length?(u=e,m):u},m.width=function(e){return arguments.length?(a=e,m):a},m.height=function(e){return arguments.length?(f=e,m):f},m.margin=function(e){return arguments.length?(i.top=typeof e.top!="undefined"?e.top:i.top,i.right=typeof e.right!="undefined"?e.right:i.right,i.bottom=typeof e.bottom!="undefined"?e.bottom:i.bottom,i.left=typeof e.left!="undefined"?e.left:i.left,m):i},m.tickFormat=function(e){return arguments.length?(l=e,m):l},m.tooltips=function(e){return arguments.length?(c=e,m):c},m.tooltipContent=function(e){return arguments.length?(h=e,m):h},m.noData=function(e){return arguments.length?(p=e,m):p},m},e.models.cumulativeLineChart=function(){"use strict";function D(b){return b.each(function(b){function q(e,t){d3.select(D.container).style("cursor","ew-resize")}function R(e,t){M.x=d3.event.x,M.i=Math.round(O.invert(M.x)),rt()}function U(e,t){d3.select(D.container).style("cursor","auto"),x.index=M.i,k.stateChange(x)}function rt(){nt.data([M]);var e=D.transitionDuration();D.transitionDuration(0),D.update(),D.transitionDuration(e)}var A=d3.select(this).classed("nv-chart-"+S,!0),H=this,B=(f||parseInt(A.style("width"))||960)-u.left-u.right,j=(l||parseInt(A.style("height"))||400)-u.top-u.bottom;D.update=function(){A.transition().duration(L).call(D)},D.container=this,x.disabled=b.map(function(e){return!!e.disabled});if(!T){var F;T={};for(F in x)x[F]instanceof Array?T[F]=x[F].slice(0):T[F]=x[F]}var I=d3.behavior.drag().on("dragstart",q).on("drag",R).on("dragend",U);if(!b||!b.length||!b.filter(function(e){return e.values.length}).length){var z=A.selectAll(".nv-noData").data([N]);return z.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),z.attr("x",u.left+B/2).attr("y",u.top+j/2).text(function(e){return e}),D}A.selectAll(".nv-noData").remove(),w=t.xScale(),E=t.yScale();if(!y){var W=b.filter(function(e){return!e.disabled}).map(function(e,n){var r=d3.extent(e.values,t.y());return r[0]<-0.95&&(r[0]=-0.95),[(r[0]-r[1])/(1+r[1]),(r[1]-r[0])/(1+r[0])]}),X=[d3.min(W,function(e){return e[0]}),d3.max(W,function(e){return e[1]})];t.yDomain(X)}else t.yDomain(null);O.domain([0,b[0].values.length-1]).range([0,B]).clamp(!0);var b=P(M.i,b),V=g?"none":"all",$=A.selectAll("g.nv-wrap.nv-cumulativeLine").data([b]),J=$.enter().append("g").attr("class","nvd3 nv-wrap nv-cumulativeLine").append("g"),K=$.select("g");J.append("g").attr("class","nv-interactive"),J.append("g").attr("class","nv-x nv-axis").style("pointer-events","none"),J.append("g").attr("class","nv-y nv-axis"),J.append("g").attr("class","nv-background"),J.append("g").attr("class","nv-linesWrap").style("pointer-events",V),J.append("g").attr("class","nv-avgLinesWrap").style("pointer-events","none"),J.append("g").attr("class","nv-legendWrap"),J.append("g").attr("class","nv-controlsWrap"),c&&(i.width(B),K.select(".nv-legendWrap").datum(b).call(i),u.top!=i.height()&&(u.top=i.height(),j=(l||parseInt(A.style("height"))||400)-u.top-u.bottom),K.select(".nv-legendWrap").attr("transform","translate(0,"+ -u.top+")"));if(m){var Q=[{key:"Re-scale y-axis",disabled:!y}];s.width(140).color(["#444","#444","#444"]).rightAlign(!1).margin({top:5,right:0,bottom:5,left:20}),K.select(".nv-controlsWrap").datum(Q).attr("transform","translate(0,"+ -u.top+")").call(s)}$.attr("transform","translate("+u.left+","+u.top+")"),d&&K.select(".nv-y.nv-axis").attr("transform","translate("+B+",0)");var G=b.filter(function(e){return e.tempDisabled});$.select(".tempDisabled").remove(),G.length&&$.append("text").attr("class","tempDisabled").attr("x",B/2).attr("y","-.71em").style("text-anchor","end").text(G.map(function(e){return e.key}).join(", ")+" values cannot be calculated for this time period."),g&&(o.width(B).height(j).margin({left:u.left,top:u.top}).svgContainer(A).xScale(w),$.select(".nv-interactive").call(o)),J.select(".nv-background").append("rect"),K.select(".nv-background rect").attr("width",B).attr("height",j),t.y(function(e){return e.display.y}).width(B).height(j).color(b.map(function(e,t){return e.color||a(e,t)}).filter(function(e,t){return!b[t].disabled&&!b[t].tempDisabled}));var Y=K.select(".nv-linesWrap").datum(b.filter(function(e){return!e.disabled&&!e.tempDisabled}));Y.call(t),b.forEach(function(e,t){e.seriesIndex=t});var Z=b.filter(function(e){return!e.disabled&&!!C(e)}),et=K.select(".nv-avgLinesWrap").selectAll("line").data(Z,function(e){return e.key}),tt=function(e){var t=E(C(e));return t<0?0:t>j?j:t};et.enter().append("line").style("stroke-width",2).style("stroke-dasharray","10,10").style("stroke",function(e,n){return t.color()(e,e.seriesIndex)}).attr("x1",0).attr("x2",B).attr("y1",tt).attr("y2",tt),et.style("stroke-opacity",function(e){var t=E(C(e));return t<0||t>j?0:1}).attr("x1",0).attr("x2",B).attr("y1",tt).attr("y2",tt),et.exit().remove();var nt=Y.selectAll(".nv-indexLine").data([M]);nt.enter().append("rect").attr("class","nv-indexLine").attr("width",3).attr("x",-2).attr("fill","red").attr("fill-opacity",.5).style("pointer-events","all").call(I),nt.attr("transform",function(e){return"translate("+O(e.i)+",0)"}).attr("height",j),h&&(n.scale(w).ticks(Math.min(b[0].values.length,B/70)).tickSize(-j,0),K.select(".nv-x.nv-axis").attr("transform","translate(0,"+E.range()[0]+")"),d3.transition(K.select(".nv-x.nv-axis")).call(n)),p&&(r.scale(E).ticks(j/36).tickSize(-B,0),d3.transition(K.select(".nv-y.nv-axis")).call(r)),K.select(".nv-background rect").on("click",function(){M.x=d3.mouse(this)[0],M.i=Math.round(O.invert(M.x)),x.index=M.i,k.stateChange(x),rt()}),t.dispatch.on("elementClick",function(e){M.i=e.pointIndex,M.x=O(M.i),x.index=M.i,k.stateChange(x),rt()}),s.dispatch.on("legendClick",function(e,t){e.disabled=!e.disabled,y=!e.disabled,x.rescaleY=y,k.stateChange(x),D.update()}),i.dispatch.on("stateChange",function(e){x.disabled=e.disabled,k.stateChange(x),D.update()}),o.dispatch.on("elementMousemove",function(i){t.clearHighlights();var s,f,l,c=[];b.filter(function(e,t){return e.seriesIndex=t,!e.disabled}).forEach(function(n,r){f=e.interactiveBisect(n.values,i.pointXValue,D.x()),t.highlightPoint(r,f,!0);var o=n.values[f];if(typeof o=="undefined")return;typeof s=="undefined"&&(s=o),typeof l=="undefined"&&(l=D.xScale()(D.x()(o,f))),c.push({key:n.key,value:D.y()(o,f),color:a(n,n.seriesIndex)})});if(c.length>2){var h=D.yScale().invert(i.mouseY),p=Math.abs(D.yScale().domain()[0]-D.yScale().domain()[1]),d=.03*p,m=e.nearestValueIndex(c.map(function(e){return e.value}),h,d);m!==null&&(c[m].highlight=!0)}var g=n.tickFormat()(D.x()(s,f),f);o.tooltip.position({left:l+u.left,top:i.mouseY+u.top}).chartContainer(H.parentNode).enabled(v).valueFormatter(function(e,t){return r.tickFormat()(e)}).data({value:g,series:c})(),o.renderGuideLine(l)}),o.dispatch.on("elementMouseout",function(e){k.tooltipHide(),t.clearHighlights()}),k.on("tooltipShow",function(e){v&&_(e,H.parentNode)}),k.on("changeState",function(e){typeof e.disabled!="undefined"&&(b.forEach(function(t,n){t.disabled=e.disabled[n]}),x.disabled=e.disabled),typeof e.index!="undefined"&&(M.i=e.index,M.x=O(M.i),x.index=e.index,nt.data([M])),typeof e.rescaleY!="undefined"&&(y=e.rescaleY),D.update()})}),D}function P(e,n){return n.map(function(n,r){if(!n.values)return n;var i=t.y()(n.values[e],e);return i<-0.95&&!A?(n.tempDisabled=!0,n):(n.tempDisabled=!1,n.values= +n.values.map(function(e,n){return e.display={y:(t.y()(e,n)-i)/(1+i)},e}),n)})}var t=e.models.line(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.models.legend(),o=e.interactiveGuideline(),u={top:30,right:30,bottom:50,left:60},a=e.utils.defaultColor(),f=null,l=null,c=!0,h=!0,p=!0,d=!1,v=!0,m=!0,g=!1,y=!0,b=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" at "+t+"

    "},w,E,S=t.id(),x={index:0,rescaleY:y},T=null,N="No Data Available.",C=function(e){return e.average},k=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),L=250,A=!1;n.orient("bottom").tickPadding(7),r.orient(d?"right":"left"),s.updateState(!1);var O=d3.scale.linear(),M={i:0,x:0},_=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=b(i.series.key,a,f,i,D);e.tooltip.show([o,u],l,null,null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+u.left,e.pos[1]+u.top],k.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){k.tooltipHide(e)}),k.on("tooltipHide",function(){v&&e.tooltip.cleanup()}),D.dispatch=k,D.lines=t,D.legend=i,D.xAxis=n,D.yAxis=r,D.interactiveLayer=o,d3.rebind(D,t,"defined","isArea","x","y","xScale","yScale","size","xDomain","yDomain","xRange","yRange","forceX","forceY","interactive","clipEdge","clipVoronoi","useVoronoi","id"),D.options=e.utils.optionsFunc.bind(D),D.margin=function(e){return arguments.length?(u.top=typeof e.top!="undefined"?e.top:u.top,u.right=typeof e.right!="undefined"?e.right:u.right,u.bottom=typeof e.bottom!="undefined"?e.bottom:u.bottom,u.left=typeof e.left!="undefined"?e.left:u.left,D):u},D.width=function(e){return arguments.length?(f=e,D):f},D.height=function(e){return arguments.length?(l=e,D):l},D.color=function(t){return arguments.length?(a=e.utils.getColor(t),i.color(a),D):a},D.rescaleY=function(e){return arguments.length?(y=e,D):y},D.showControls=function(e){return arguments.length?(m=e,D):m},D.useInteractiveGuideline=function(e){return arguments.length?(g=e,e===!0&&(D.interactive(!1),D.useVoronoi(!1)),D):g},D.showLegend=function(e){return arguments.length?(c=e,D):c},D.showXAxis=function(e){return arguments.length?(h=e,D):h},D.showYAxis=function(e){return arguments.length?(p=e,D):p},D.rightAlignYAxis=function(e){return arguments.length?(d=e,r.orient(e?"right":"left"),D):d},D.tooltips=function(e){return arguments.length?(v=e,D):v},D.tooltipContent=function(e){return arguments.length?(b=e,D):b},D.state=function(e){return arguments.length?(x=e,D):x},D.defaultState=function(e){return arguments.length?(T=e,D):T},D.noData=function(e){return arguments.length?(N=e,D):N},D.average=function(e){return arguments.length?(C=e,D):C},D.transitionDuration=function(e){return arguments.length?(L=e,D):L},D.noErrorCheck=function(e){return arguments.length?(A=e,D):A},D},e.models.discreteBar=function(){"use strict";function E(e){return e.each(function(e){var i=n-t.left-t.right,E=r-t.top-t.bottom,S=d3.select(this);e.forEach(function(e,t){e.values.forEach(function(e){e.series=t})});var T=p&&d?[]:e.map(function(e){return e.values.map(function(e,t){return{x:u(e,t),y:a(e,t),y0:e.y0}})});s.domain(p||d3.merge(T).map(function(e){return e.x})).rangeBands(v||[0,i],.1),o.domain(d||d3.extent(d3.merge(T).map(function(e){return e.y}).concat(f))),c?o.range(m||[E-(o.domain()[0]<0?12:0),o.domain()[1]>0?12:0]):o.range(m||[E,0]),b=b||s,w=w||o.copy().range([o(0),o(0)]);var N=S.selectAll("g.nv-wrap.nv-discretebar").data([e]),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-discretebar"),k=C.append("g"),L=N.select("g");k.append("g").attr("class","nv-groups"),N.attr("transform","translate("+t.left+","+t.top+")");var A=N.select(".nv-groups").selectAll(".nv-group").data(function(e){return e},function(e){return e.key});A.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),A.exit().transition().style("stroke-opacity",1e-6).style("fill-opacity",1e-6).remove(),A.attr("class",function(e,t){return"nv-group nv-series-"+t}).classed("hover",function(e){return e.hover}),A.transition().style("stroke-opacity",1).style("fill-opacity",.75);var O=A.selectAll("g.nv-bar").data(function(e){return e.values});O.exit().remove();var M=O.enter().append("g").attr("transform",function(e,t,n){return"translate("+(s(u(e,t))+s.rangeBand()*.05)+", "+o(0)+")"}).on("mouseover",function(t,n){d3.select(this).classed("hover",!0),g.elementMouseover({value:a(t,n),point:t,series:e[t.series],pos:[s(u(t,n))+s.rangeBand()*(t.series+.5)/e.length,o(a(t,n))],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("mouseout",function(t,n){d3.select(this).classed("hover",!1),g.elementMouseout({value:a(t,n),point:t,series:e[t.series],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("click",function(t,n){g.elementClick({value:a(t,n),point:t,series:e[t.series],pos:[s(u(t,n))+s.rangeBand()*(t.series+.5)/e.length,o(a(t,n))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()}).on("dblclick",function(t,n){g.elementDblClick({value:a(t,n),point:t,series:e[t.series],pos:[s(u(t,n))+s.rangeBand()*(t.series+.5)/e.length,o(a(t,n))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()});M.append("rect").attr("height",0).attr("width",s.rangeBand()*.9/e.length),c?(M.append("text").attr("text-anchor","middle"),O.select("text").text(function(e,t){return h(a(e,t))}).transition().attr("x",s.rangeBand()*.9/2).attr("y",function(e,t){return a(e,t)<0?o(a(e,t))-o(0)+12:-4})):O.selectAll("text").remove(),O.attr("class",function(e,t){return a(e,t)<0?"nv-bar negative":"nv-bar positive"}).style("fill",function(e,t){return e.color||l(e,t)}).style("stroke",function(e,t){return e.color||l(e,t)}).select("rect").attr("class",y).transition().attr("width",s.rangeBand()*.9/e.length),O.transition().attr("transform",function(e,t){var n=s(u(e,t))+s.rangeBand()*.05,r=a(e,t)<0?o(0):o(0)-o(a(e,t))<1?o(0)-1:o(a(e,t));return"translate("+n+", "+r+")"}).select("rect").attr("height",function(e,t){return Math.max(Math.abs(o(a(e,t))-o(d&&d[0]||0))||1)}),b=s.copy(),w=o.copy()}),E}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=Math.floor(Math.random()*1e4),s=d3.scale.ordinal(),o=d3.scale.linear(),u=function(e){return e.x},a=function(e){return e.y},f=[0],l=e.utils.defaultColor(),c=!1,h=d3.format(",.2f"),p,d,v,m,g=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout"),y="discreteBar",b,w;return E.dispatch=g,E.options=e.utils.optionsFunc.bind(E),E.x=function(e){return arguments.length?(u=e,E):u},E.y=function(e){return arguments.length?(a=e,E):a},E.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,E):t},E.width=function(e){return arguments.length?(n=e,E):n},E.height=function(e){return arguments.length?(r=e,E):r},E.xScale=function(e){return arguments.length?(s=e,E):s},E.yScale=function(e){return arguments.length?(o=e,E):o},E.xDomain=function(e){return arguments.length?(p=e,E):p},E.yDomain=function(e){return arguments.length?(d=e,E):d},E.xRange=function(e){return arguments.length?(v=e,E):v},E.yRange=function(e){return arguments.length?(m=e,E):m},E.forceY=function(e){return arguments.length?(f=e,E):f},E.color=function(t){return arguments.length?(l=e.utils.getColor(t),E):l},E.id=function(e){return arguments.length?(i=e,E):i},E.showValues=function(e){return arguments.length?(c=e,E):c},E.valueFormat=function(e){return arguments.length?(h=e,E):h},E.rectClass=function(e){return arguments.length?(y=e,E):y},E},e.models.discreteBarChart=function(){"use strict";function w(e){return e.each(function(e){var u=d3.select(this),p=this,E=(s||parseInt(u.style("width"))||960)-i.left-i.right,S=(o||parseInt(u.style("height"))||400)-i.top-i.bottom;w.update=function(){g.beforeUpdate(),u.transition().duration(y).call(w)},w.container=this;if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var T=u.selectAll(".nv-noData").data([m]);return T.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),T.attr("x",i.left+E/2).attr("y",i.top+S/2).text(function(e){return e}),w}u.selectAll(".nv-noData").remove(),d=t.xScale(),v=t.yScale().clamp(!0);var N=u.selectAll("g.nv-wrap.nv-discreteBarWithAxes").data([e]),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-discreteBarWithAxes").append("g"),k=C.append("defs"),L=N.select("g");C.append("g").attr("class","nv-x nv-axis"),C.append("g").attr("class","nv-y nv-axis").append("g").attr("class","nv-zeroLine").append("line"),C.append("g").attr("class","nv-barsWrap"),L.attr("transform","translate("+i.left+","+i.top+")"),l&&L.select(".nv-y.nv-axis").attr("transform","translate("+E+",0)"),t.width(E).height(S);var A=L.select(".nv-barsWrap").datum(e.filter(function(e){return!e.disabled}));A.transition().call(t),k.append("clipPath").attr("id","nv-x-label-clip-"+t.id()).append("rect"),L.select("#nv-x-label-clip-"+t.id()+" rect").attr("width",d.rangeBand()*(c?2:1)).attr("height",16).attr("x",-d.rangeBand()/(c?1:2));if(a){n.scale(d).ticks(E/100).tickSize(-S,0),L.select(".nv-x.nv-axis").attr("transform","translate(0,"+(v.range()[0]+(t.showValues()&&v.domain()[0]<0?16:0))+")"),L.select(".nv-x.nv-axis").transition().call(n);var O=L.select(".nv-x.nv-axis").selectAll("g");c&&O.selectAll("text").attr("transform",function(e,t,n){return"translate(0,"+(n%2==0?"5":"17")+")"})}f&&(r.scale(v).ticks(S/36).tickSize(-E,0),L.select(".nv-y.nv-axis").transition().call(r)),L.select(".nv-zeroLine line").attr("x1",0).attr("x2",E).attr("y1",v(0)).attr("y2",v(0)),g.on("tooltipShow",function(e){h&&b(e,p.parentNode)})}),w}var t=e.models.discreteBar(),n=e.models.axis(),r=e.models.axis(),i={top:15,right:10,bottom:50,left:60},s=null,o=null,u=e.utils.getColor(),a=!0,f=!0,l=!1,c=!1,h=!0,p=function(e,t,n,r,i){return"

    "+t+"

    "+"

    "+n+"

    "},d,v,m="No Data Available.",g=d3.dispatch("tooltipShow","tooltipHide","beforeUpdate"),y=250;n.orient("bottom").highlightZero(!1).showMaxMin(!1).tickFormat(function(e){return e}),r.orient(l?"right":"left").tickFormat(d3.format(",.1f"));var b=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=p(i.series.key,a,f,i,w);e.tooltip.show([o,u],l,i.value<0?"n":"s",null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+i.left,e.pos[1]+i.top],g.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){g.tooltipHide(e)}),g.on("tooltipHide",function(){h&&e.tooltip.cleanup()}),w.dispatch=g,w.discretebar=t,w.xAxis=n,w.yAxis=r,d3.rebind(w,t,"x","y","xDomain","yDomain","xRange","yRange","forceX","forceY","id","showValues","valueFormat"),w.options=e.utils.optionsFunc.bind(w),w.margin=function(e){return arguments.length?(i.top=typeof e.top!="undefined"?e.top:i.top,i.right=typeof e.right!="undefined"?e.right:i.right,i.bottom=typeof e.bottom!="undefined"?e.bottom:i.bottom,i.left=typeof e.left!="undefined"?e.left:i.left,w):i},w.width=function(e){return arguments.length?(s=e,w):s},w.height=function(e){return arguments.length?(o=e,w):o},w.color=function(n){return arguments.length?(u=e.utils.getColor(n),t.color(u),w):u},w.showXAxis=function(e){return arguments.length?(a=e,w):a},w.showYAxis=function(e){return arguments.length?(f=e,w):f},w.rightAlignYAxis=function(e){return arguments.length?(l=e,r.orient(e?"right":"left"),w):l},w.staggerLabels=function(e){return arguments.length?(c=e,w):c},w.tooltips=function(e){return arguments.length?(h=e,w):h},w.tooltipContent=function(e){return arguments.length?(p=e,w):p},w.noData=function(e){return arguments.length?(m=e,w):m},w.transitionDuration=function(e){return arguments.length?(y=e,w):y},w},e.models.distribution=function(){"use strict";function l(e){return e.each(function(e){var a=n-(i==="x"?t.left+t.right:t.top+t.bottom),l=i=="x"?"y":"x",c=d3.select(this);f=f||u;var h=c.selectAll("g.nv-distribution").data([e]),p=h.enter().append("g").attr("class","nvd3 nv-distribution"),d=p.append("g"),v=h.select("g");h.attr("transform","translate("+t.left+","+t.top+")");var m=v.selectAll("g.nv-dist").data(function(e){return e},function(e){return e.key});m.enter().append("g"),m.attr("class",function(e,t){return"nv-dist nv-series-"+t}).style("stroke",function(e,t){return o(e,t)});var g=m.selectAll("line.nv-dist"+i).data(function(e){return e.values});g.enter().append("line").attr(i+"1",function(e,t){return f(s(e,t))}).attr(i+"2",function(e,t){return f(s(e,t))}),m.exit().selectAll("line.nv-dist"+i).transition().attr(i+"1",function(e,t){return u(s(e,t))}).attr(i+"2",function(e,t){return u(s(e,t))}).style("stroke-opacity",0).remove(),g.attr("class",function(e,t){return"nv-dist"+i+" nv-dist"+i+"-"+t}).attr(l+"1",0).attr(l+"2",r),g.transition().attr(i+"1",function(e,t){return u(s(e,t))}).attr(i+"2",function(e,t){return u(s(e,t))}),f=u.copy()}),l}var t={top:0,right:0,bottom:0,left:0},n=400,r=8,i="x",s=function(e){return e[i]},o=e.utils.defaultColor(),u=d3.scale.linear(),a,f;return l.options=e.utils.optionsFunc.bind(l),l.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,l):t},l.width=function(e){return arguments.length?(n=e,l):n},l.axis=function(e){return arguments.length?(i=e,l):i},l.size=function(e){return arguments.length?(r=e,l):r},l.getData=function(e){return arguments.length?(s=d3.functor(e),l):s},l.scale=function(e){return arguments.length?(u=e,l):u},l.color=function(t){return arguments.length?(o=e.utils.getColor(t),l):o},l},e.models.historicalBar=function(){"use strict";function w(E){return E.each(function(w){var E=n-t.left-t.right,S=r-t.top-t.bottom,T=d3.select(this);s.domain(d||d3.extent(w[0].values.map(u).concat(f))),c?s.range(m||[E*.5/w[0].values.length,E*(w[0].values.length-.5)/w[0].values.length]):s.range(m||[0,E]),o.domain(v||d3.extent(w[0].values.map(a).concat(l))).range(g||[S,0]),s.domain()[0]===s.domain()[1]&&(s.domain()[0]?s.domain([s.domain()[0]-s.domain()[0]*.01,s.domain()[1]+s.domain()[1]*.01]):s.domain([-1,1])),o.domain()[0]===o.domain()[1]&&(o.domain()[0]?o.domain([o.domain()[0]+o.domain()[0]*.01,o.domain()[1]-o.domain()[1]*.01]):o.domain([-1,1]));var N=T.selectAll("g.nv-wrap.nv-historicalBar-"+i).data([w[0].values]),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-historicalBar-"+i),k=C.append("defs"),L=C.append("g"),A=N.select("g");L.append("g").attr("class","nv-bars"),N.attr("transform","translate("+t.left+","+t.top+")"),T.on("click",function(e,t){y.chartClick({data:e,index:t,pos:d3.event,id:i})}),k.append("clipPath").attr("id","nv-chart-clip-path-"+i).append("rect"),N.select("#nv-chart-clip-path-"+i+" rect").attr("width",E).attr("height",S),A.attr("clip-path",h?"url(#nv-chart-clip-path-"+i+")":"");var O=N.select(".nv-bars").selectAll(".nv-bar").data(function(e){return e},function(e,t){return u(e,t)});O.exit().remove();var M=O.enter().append("rect").attr("x",0).attr("y",function(t,n){return e.utils.NaNtoZero(o(Math.max(0,a(t,n))))}).attr("height",function(t,n){return e.utils.NaNtoZero(Math.abs(o(a(t,n))-o(0)))}).attr("transform",function(e,t){return"translate("+(s(u(e,t))-E/w[0].values.length*.45)+",0)"}).on("mouseover",function(e,t){if(!b)return;d3.select(this).classed("hover",!0),y.elementMouseover({point:e,series:w[0],pos:[s(u(e,t)),o(a(e,t))],pointIndex:t,seriesIndex:0,e:d3.event})}).on("mouseout",function(e,t){if(!b)return;d3.select(this).classed("hover",!1),y.elementMouseout({point:e,series:w[0],pointIndex:t,seriesIndex:0,e:d3.event})}).on("click",function(e,t){if(!b)return;y.elementClick({value:a(e,t),data:e,index:t,pos:[s(u(e,t)),o(a(e,t))],e:d3.event,id:i}),d3.event.stopPropagation()}).on("dblclick",function(e,t){if(!b)return;y.elementDblClick({value:a(e,t),data:e,index:t,pos:[s(u(e,t)),o(a(e,t))],e:d3.event,id:i}),d3.event.stopPropagation()});O.attr("fill",function(e,t){return p(e,t)}).attr("class",function(e,t,n){return(a(e,t)<0?"nv-bar negative":"nv-bar positive")+" nv-bar-"+n+"-"+t}).transition().attr("transform",function(e,t){return"translate("+(s(u(e,t))-E/w[0].values.length*.45)+",0)"}).attr("width",E/w[0].values.length*.9),O.transition().attr("y",function(t,n){var r=a(t,n)<0?o(0):o(0)-o(a(t,n))<1?o(0)-1:o(a(t,n));return e.utils.NaNtoZero(r)}).attr("height",function(t,n){return e.utils.NaNtoZero(Math.max(Math.abs(o(a(t,n))-o(0)),1))})}),w}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=Math.floor(Math.random()*1e4),s=d3.scale.linear(),o=d3.scale.linear(),u=function(e){return e.x},a=function(e){return e.y},f=[],l=[0],c=!1,h=!0,p=e.utils.defaultColor(),d,v,m,g,y=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout"),b=!0;return w.highlightPoint=function(e,t){d3.select(".nv-historicalBar-"+i).select(".nv-bars .nv-bar-0-"+e).classed("hover",t)},w.clearHighlights=function(){d3.select(".nv-historicalBar-"+i).select(".nv-bars .nv-bar.hover").classed("hover",!1)},w.dispatch=y,w.options=e.utils.optionsFunc.bind(w),w.x=function(e){return arguments.length?(u=e,w):u},w.y=function(e){return arguments.length?(a=e,w):a},w.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,w):t},w.width=function(e){return arguments.length?(n=e,w):n},w.height=function(e){return arguments.length?(r=e,w):r},w.xScale=function(e){return arguments.length?(s=e,w):s},w.yScale=function(e){return arguments.length?(o=e,w):o},w.xDomain=function(e){return arguments.length?(d=e,w):d},w.yDomain=function(e){return arguments.length?(v=e,w):v},w.xRange=function(e){return arguments.length?(m=e,w):m},w.yRange=function(e){return arguments.length?(g=e,w):g},w.forceX=function(e){return arguments.length?(f=e,w):f},w.forceY=function(e){return arguments.length?(l=e,w):l},w.padData=function(e){return arguments.length?(c=e,w):c},w.clipEdge=function(e){return arguments.length?(h=e,w):h},w.color=function(t){return arguments.length?(p=e.utils.getColor(t),w):p},w.id=function(e){return arguments.length?(i=e,w):i},w.interactive=function(e){return arguments.length?(b=!1,w):b},w},e.models.historicalBarChart=function(){"use strict";function x(e){return e.each(function(d){var T=d3.select(this),N=this,C=(u||parseInt(T.style("width"))||960)-s.left-s.right,k=(a||parseInt(T.style("height"))||400)-s.top-s.bottom;x.update=function(){T.transition().duration(E).call(x)},x.container=this,g.disabled=d.map(function(e){return!!e.disabled});if(!y){var L;y={};for(L in g)g[L]instanceof Array?y[L]=g[L].slice(0):y[L]=g[L]}if(!d||!d.length||!d.filter(function(e){return e.values.length}).length){var A=T.selectAll(".nv-noData").data([b]);return A.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),A.attr("x",s.left+C/2).attr("y",s.top+k/2).text(function(e){return e}),x}T.selectAll(".nv-noData").remove(),v=t.xScale(),m=t.yScale();var O=T.selectAll("g.nv-wrap.nv-historicalBarChart").data([d]),M=O.enter().append("g").attr("class","nvd3 nv-wrap nv-historicalBarChart").append("g"),_=O.select("g");M.append("g").attr("class","nv-x nv-axis"),M.append("g").attr("class","nv-y nv-axis"),M.append("g").attr("class","nv-barsWrap"),M.append("g").attr("class","nv-legendWrap"),f&&(i.width(C),_.select(".nv-legendWrap").datum(d).call(i),s.top!=i.height()&&(s.top=i.height(),k=(a||parseInt(T.style("height"))||400)-s.top-s.bottom),O.select(".nv-legendWrap").attr("transform","translate(0,"+ -s.top+")")),O.attr("transform","translate("+s.left+","+s.top+")"),h&&_.select(".nv-y.nv-axis").attr("transform","translate("+C+",0)"),t.width(C).height(k).color(d.map(function(e,t){return e.color||o(e,t)}).filter(function(e,t){return!d[t].disabled}));var D=_.select(".nv-barsWrap").datum(d.filter(function(e){return!e.disabled}));D.transition().call(t),l&&(n.scale(v).tickSize(-k,0),_.select(".nv-x.nv-axis").attr("transform","translate(0,"+m.range()[0]+")"),_.select(".nv-x.nv-axis").transition().call(n)),c&&(r.scale(m).ticks(k/36).tickSize(-C,0),_.select(".nv-y.nv-axis").transition().call(r)),i.dispatch.on("legendClick",function(t,n){t.disabled=!t.disabled,d.filter(function(e){return!e.disabled}).length||d.map(function(e){return e.disabled=!1,O.selectAll(".nv-series").classed("disabled",!1),e}),g.disabled=d.map(function(e){return!!e.disabled}),w.stateChange(g),e.transition().call(x)}),i.dispatch.on("legendDblclick",function(e){d.forEach(function(e){e.disabled=!0}),e.disabled=!1,g.disabled=d.map(function(e){return!!e.disabled}),w.stateChange(g),x.update()}),w.on("tooltipShow",function(e){p&&S(e,N.parentNode)}),w.on("changeState",function(e){typeof e.disabled!="undefined"&&(d.forEach(function(t,n){t.disabled=e.disabled[n]}),g.disabled=e.disabled),x.update()})}),x}var t=e.models.historicalBar(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s={top:30,right:90,bottom:50,left:90},o=e.utils.defaultColor(),u=null,a=null,f=!1,l=!0,c=!0,h=!1,p=!0,d=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" at "+t+"

    "},v,m,g={},y=null,b="No Data Available.",w=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),E=250;n.orient("bottom").tickPadding(7),r.orient(h?"right":"left");var S=function(i,s){if(s){var o=d3.select(s).select("svg"),u=o.node()?o.attr("viewBox"):null;if(u){u=u.split(" ");var a=parseInt(o.style("width"))/u[2];i.pos[0]=i.pos[0]*a,i.pos[1]=i.pos[1]*a}}var f=i.pos[0]+(s.offsetLeft||0),l=i.pos[1]+(s.offsetTop||0),c=n.tickFormat()(t.x()(i.point,i.pointIndex)),h=r.tickFormat()(t.y()(i.point,i.pointIndex)),p=d(i.series.key,c,h,i,x);e.tooltip.show([f,l],p,null,null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+s.left,e.pos[1]+s.top],w.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){w.tooltipHide(e)}),w.on("tooltipHide",function(){p&&e.tooltip.cleanup()}),x.dispatch=w,x.bars=t,x.legend=i,x.xAxis=n,x.yAxis=r,d3.rebind(x,t,"defined","isArea","x","y","size","xScale","yScale","xDomain","yDomain","xRange","yRange","forceX","forceY","interactive","clipEdge","clipVoronoi","id","interpolate","highlightPoint","clearHighlights","interactive"),x.options=e.utils.optionsFunc.bind(x),x.margin=function(e){return arguments.length?(s.top=typeof e.top!="undefined"?e.top:s.top,s.right=typeof e.right!="undefined"?e.right:s.right,s.bottom=typeof e.bottom!="undefined"?e.bottom:s.bottom,s.left=typeof e.left!="undefined"?e.left:s.left,x):s},x.width=function(e){return arguments.length?(u=e,x):u},x.height=function(e){return arguments.length?(a=e,x):a},x.color=function(t){return arguments.length?(o=e.utils.getColor(t),i.color(o),x):o},x.showLegend=function(e){return arguments.length?(f=e,x):f},x.showXAxis=function(e){return arguments.length?(l=e,x):l},x.showYAxis=function(e){return arguments.length?(c=e,x):c},x.rightAlignYAxis=function(e){return arguments.length?(h=e,r.orient(e?"right":"left"),x):h},x.tooltips=function(e){return arguments.length?(p=e,x):p},x.tooltipContent=function(e){return arguments.length?(d=e,x):d},x.state=function(e){return arguments.length?(g=e,x):g},x.defaultState=function(e){return arguments.length?(y=e,x):y},x.noData=function(e){return arguments.length?(b=e,x):b},x.transitionDuration=function(e){return arguments.length?(E=e,x):E},x},e.models.indentedTree=function(){"use strict";function g(e){return e.each(function(e){function k(e,t,n){d3.event.stopPropagation();if(d3.event.shiftKey&&!n)return d3.event.shiftKey=!1,e.values&&e.values.forEach(function(e){(e.values||e._values)&&k(e,0,!0)}),!0;if(!O(e))return!0;e.values?(e._values=e.values,e.values=null):(e.values=e._values,e._values=null),g.update()}function L(e){return e._values&&e._values.length?h:e.values&&e.values.length?p:""}function A(e){return e._values&&e._values.length}function O(e){var t=e.values||e._values;return t&&t.length}var t=1,n=d3.select(this),i=d3.layout.tree().children(function(e){return e.values}).size([r,f]);g.update=function(){n.transition().duration(600).call(g)},e[0]||(e[0]={key:a});var s=i.nodes(e[0]),y=d3.select(this).selectAll("div").data([[s]]),b=y.enter().append("div").attr("class","nvd3 nv-wrap nv-indentedtree"),w=b.append("table"),E=y.select("table").attr("width","100%").attr("class",c);if(o){var S=w.append("thead"),x=S.append("tr");l.forEach(function(e){x.append("th").attr("width",e.width?e.width:"10%").style("text-align",e.type=="numeric"?"right":"left").append("span").text(e.label)})}var T=E.selectAll("tbody").data(function(e){return e});T.enter().append("tbody"),t=d3.max(s,function(e){return e.depth}),i.size([r,t*f]);var N=T.selectAll("tr").data(function(e){return e.filter(function(e){return u&&!e.children?u(e):!0})},function(e,t){return e.id||e.id||++m});N.exit().remove(),N.select("img.nv-treeicon").attr("src",L).classed("folded",A);var C=N.enter().append("tr");l.forEach(function(e,t){var n=C.append("td").style("padding-left",function(e){return(t?0:e.depth*f+12+(L(e)?0:16))+"px"},"important").style("text-align",e.type=="numeric"?"right":"left");t==0&&n.append("img").classed("nv-treeicon",!0).classed("nv-folded",A).attr("src",L).style("width","14px").style("height","14px").style("padding","0 1px").style("display",function(e){return L(e)?"inline-block":"none"}).on("click",k),n.each(function(n){!t&&v(n)?d3.select(this).append("a").attr("href",v).attr("class",d3.functor(e.classes)).append("span"):d3.select(this).append("span"),d3.select(this).select("span").attr("class",d3.functor(e.classes)).text(function(t){return e.format?e.format(t):t[e.key]||"-"})}),e.showCount&&(n.append("span").attr("class","nv-childrenCount"),N.selectAll("span.nv-childrenCount").text(function(e){return e.values&&e.values.length||e._values&&e._values.length?"("+(e.values&&e.values.filter(function(e){return u?u(e):!0}).length||e._values&&e._values.filter(function(e){return u?u(e):!0}).length||0)+")":""}))}),N.order().on("click",function(e){d.elementClick({row:this,data:e,pos:[e.x,e.y]})}).on("dblclick",function(e){d.elementDblclick({row:this,data:e,pos:[e.x,e.y]})}).on("mouseover",function(e){d.elementMouseover({row:this,data:e,pos:[e.x,e.y]})}).on("mouseout",function(e){d.elementMouseout({row:this,data:e,pos:[e.x,e.y]})})}),g}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=e.utils.defaultColor(),s=Math.floor(Math.random()*1e4),o=!0,u=!1,a="No Data Available.",f=20,l=[{key:"key",label:"Name",type:"text"}],c=null,h="images/grey-plus.png",p="images/grey-minus.png",d=d3.dispatch("elementClick","elementDblclick","elementMouseover","elementMouseout"),v=function(e){return e.url},m=0;return g.options=e.utils.optionsFunc.bind(g),g.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,g):t},g.width=function(e){return arguments.length?(n=e,g):n},g.height=function(e){return arguments.length?(r=e,g):r},g.color=function(t){return arguments.length?(i=e.utils.getColor(t),scatter.color(i),g):i},g.id=function(e){return arguments.length?(s=e,g):s},g.header=function(e){return arguments.length?(o=e,g):o},g.noData=function(e){return arguments.length?(a=e,g):a},g.filterZero=function(e){return arguments.length?(u=e,g):u},g.columns=function(e){return arguments.length?(l=e,g):l},g.tableClass=function(e){return arguments.length?(c=e,g):c},g.iconOpen=function(e){return arguments.length?(h=e,g):h},g.iconClose=function(e){return arguments.length?(p=e,g):p},g.getUrl=function(e){return arguments.length?(v=e,g):v},g},e.models.legend=function(){"use strict";function c(h){return h.each(function(c){var h=n-t.left-t.right,p=d3.select(this),d=p.selectAll("g.nv-legend").data([c]),v=d.enter().append("g").attr("class","nvd3 nv-legend").append("g"),m=d.select("g");d.attr("transform","translate("+t.left+","+t.top+")");var g=m.selectAll(".nv-series").data(function(e){return e}),y=g.enter().append("g").attr("class","nv-series").on("mouseover",function(e,t){l.legendMouseover(e,t)}).on("mouseout",function(e,t){l.legendMouseout(e,t)}).on("click",function(e,t){l.legendClick(e,t),a&&(f?(c.forEach(function(e){e.disabled=!0}),e.disabled=!1):(e.disabled=!e.disabled,c.every(function(e){return e.disabled})&&c.forEach(function(e){e.disabled=!1})),l.stateChange({disabled:c.map(function(e){return!!e.disabled})}))}).on("dblclick",function(e,t){l.legendDblclick(e,t),a&&(c.forEach(function(e){e.disabled=!0}),e.disabled=!1,l.stateChange({disabled:c.map(function(e){return!!e.disabled})}))});y.append("circle").style("stroke-width",2).attr("class","nv-legend-symbol").attr("r",5),y.append("text").attr("text-anchor","start").attr("class","nv-legend-text").attr("dy",".32em").attr("dx","8"),g.classed("disabled",function(e){return e.disabled}),g.exit().remove(),g.select("circle").style("fill",function(e,t){return e.color||s(e,t)}).style("stroke",function(e,t){return e.color||s(e,t)}),g.select("text").text(i);if(o){var b=[];g.each(function(t,n){var r=d3.select(this).select("text"),i;try{i=r.getComputedTextLength();if(i<=0)throw Error()}catch(s){i=e.utils.calcApproxTextWidth(r)}b.push(i+28)});var w=0,E=0,S=[];while(Eh&&w>1){S=[],w--;for(var x=0;x(S[x%w]||0)&&(S[x%w]=b[x]);E=S.reduce(function(e,t,n,r){return e+t})}var T=[];for(var N=0,C=0;NA&&(A=L),"translate("+O+","+k+")"}),m.attr("transform","translate("+(n-t.right-A)+","+t.top+")"),r=t.top+t.bottom+k+15}}),c}var t={top:5,right:0,bottom:5,left:0},n=400,r=20,i=function(e){return e.key},s=e.utils.defaultColor(),o=!0,u=!0,a=!0,f=!1,l=d3.dispatch("legendClick","legendDblclick","legendMouseover","legendMouseout","stateChange");return c.dispatch=l,c.options=e.utils.optionsFunc.bind(c),c.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,c):t},c.width=function(e){return arguments.length?(n=e,c):n},c.height=function(e){return arguments.length?(r=e,c):r},c.key=function(e){return arguments.length?(i=e,c):i},c.color=function(t){return arguments.length?(s=e.utils.getColor(t),c):s},c.align=function(e){return arguments.length?(o=e,c):o},c.rightAlign=function(e){return arguments.length?(u=e,c):u},c.updateState=function(e){return arguments.length?(a=e,c):a},c.radioButtonMode=function(e){return arguments.length?(f=e,c):f},c},e.models.line=function(){"use strict";function m(g){return g.each(function(m){var g=r-n.left-n.right,b=i-n.top-n.bottom,w=d3.select(this);c=t.xScale(),h=t.yScale(),d=d||c,v=v||h;var E=w.selectAll("g.nv-wrap.nv-line").data([m]),S=E.enter().append("g").attr("class","nvd3 nv-wrap nv-line"),T=S.append("defs"),N=S.append("g"),C=E.select("g");N.append("g").attr("class","nv-groups"),N.append("g").attr("class","nv-scatterWrap"),E.attr("transform","translate("+n.left+","+n.top+")"),t.width(g).height(b);var k=E.select(".nv-scatterWrap");k.transition().call(t),T.append("clipPath").attr("id","nv-edge-clip-"+t.id()).append("rect"),E.select("#nv-edge-clip-"+t.id()+" rect").attr("width",g).attr("height",b),C.attr("clip-path",l?"url(#nv-edge-clip-"+t.id()+")":""),k.attr("clip-path",l?"url(#nv-edge-clip-"+t.id()+")":"");var L=E.select(".nv-groups").selectAll(".nv-group").data(function(e){return e},function(e){return e.key});L.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),L.exit().remove(),L.attr("class",function(e,t){return"nv-group nv-series-"+t}).classed("hover",function(e){return e.hover}).style("fill",function(e,t){return s(e,t)}).style("stroke",function(e,t){return s(e,t)}),L.transition().style("stroke-opacity",1).style("fill-opacity",.5);var A=L.selectAll("path.nv-area").data(function(e){return f(e)?[e]:[]});A.enter().append("path").attr("class","nv-area").attr("d",function(t){return d3.svg.area().interpolate(p).defined(a).x(function(t,n){return e. +utils.NaNtoZero(d(o(t,n)))}).y0(function(t,n){return e.utils.NaNtoZero(v(u(t,n)))}).y1(function(e,t){return v(h.domain()[0]<=0?h.domain()[1]>=0?0:h.domain()[1]:h.domain()[0])}).apply(this,[t.values])}),L.exit().selectAll("path.nv-area").remove(),A.transition().attr("d",function(t){return d3.svg.area().interpolate(p).defined(a).x(function(t,n){return e.utils.NaNtoZero(c(o(t,n)))}).y0(function(t,n){return e.utils.NaNtoZero(h(u(t,n)))}).y1(function(e,t){return h(h.domain()[0]<=0?h.domain()[1]>=0?0:h.domain()[1]:h.domain()[0])}).apply(this,[t.values])});var O=L.selectAll("path.nv-line").data(function(e){return[e.values]});O.enter().append("path").attr("class","nv-line").attr("d",d3.svg.line().interpolate(p).defined(a).x(function(t,n){return e.utils.NaNtoZero(d(o(t,n)))}).y(function(t,n){return e.utils.NaNtoZero(v(u(t,n)))})),O.transition().attr("d",d3.svg.line().interpolate(p).defined(a).x(function(t,n){return e.utils.NaNtoZero(c(o(t,n)))}).y(function(t,n){return e.utils.NaNtoZero(h(u(t,n)))})),d=c.copy(),v=h.copy()}),m}var t=e.models.scatter(),n={top:0,right:0,bottom:0,left:0},r=960,i=500,s=e.utils.defaultColor(),o=function(e){return e.x},u=function(e){return e.y},a=function(e,t){return!isNaN(u(e,t))&&u(e,t)!==null},f=function(e){return e.area},l=!1,c,h,p="linear";t.size(16).sizeDomain([16,256]);var d,v;return m.dispatch=t.dispatch,m.scatter=t,d3.rebind(m,t,"id","interactive","size","xScale","yScale","zScale","xDomain","yDomain","xRange","yRange","sizeDomain","forceX","forceY","forceSize","clipVoronoi","useVoronoi","clipRadius","padData","highlightPoint","clearHighlights"),m.options=e.utils.optionsFunc.bind(m),m.margin=function(e){return arguments.length?(n.top=typeof e.top!="undefined"?e.top:n.top,n.right=typeof e.right!="undefined"?e.right:n.right,n.bottom=typeof e.bottom!="undefined"?e.bottom:n.bottom,n.left=typeof e.left!="undefined"?e.left:n.left,m):n},m.width=function(e){return arguments.length?(r=e,m):r},m.height=function(e){return arguments.length?(i=e,m):i},m.x=function(e){return arguments.length?(o=e,t.x(e),m):o},m.y=function(e){return arguments.length?(u=e,t.y(e),m):u},m.clipEdge=function(e){return arguments.length?(l=e,m):l},m.color=function(n){return arguments.length?(s=e.utils.getColor(n),t.color(s),m):s},m.interpolate=function(e){return arguments.length?(p=e,m):p},m.defined=function(e){return arguments.length?(a=e,m):a},m.isArea=function(e){return arguments.length?(f=d3.functor(e),m):f},m},e.models.lineChart=function(){"use strict";function N(m){return m.each(function(m){var C=d3.select(this),k=this,L=(a||parseInt(C.style("width"))||960)-o.left-o.right,A=(f||parseInt(C.style("height"))||400)-o.top-o.bottom;N.update=function(){C.transition().duration(x).call(N)},N.container=this,b.disabled=m.map(function(e){return!!e.disabled});if(!w){var O;w={};for(O in b)b[O]instanceof Array?w[O]=b[O].slice(0):w[O]=b[O]}if(!m||!m.length||!m.filter(function(e){return e.values.length}).length){var M=C.selectAll(".nv-noData").data([E]);return M.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),M.attr("x",o.left+L/2).attr("y",o.top+A/2).text(function(e){return e}),N}C.selectAll(".nv-noData").remove(),g=t.xScale(),y=t.yScale();var _=C.selectAll("g.nv-wrap.nv-lineChart").data([m]),D=_.enter().append("g").attr("class","nvd3 nv-wrap nv-lineChart").append("g"),P=_.select("g");D.append("rect").style("opacity",0),D.append("g").attr("class","nv-x nv-axis"),D.append("g").attr("class","nv-y nv-axis"),D.append("g").attr("class","nv-linesWrap"),D.append("g").attr("class","nv-legendWrap"),D.append("g").attr("class","nv-interactive"),P.select("rect").attr("width",L).attr("height",A>0?A:0),l&&(i.width(L),P.select(".nv-legendWrap").datum(m).call(i),o.top!=i.height()&&(o.top=i.height(),A=(f||parseInt(C.style("height"))||400)-o.top-o.bottom),_.select(".nv-legendWrap").attr("transform","translate(0,"+ -o.top+")")),_.attr("transform","translate("+o.left+","+o.top+")"),p&&P.select(".nv-y.nv-axis").attr("transform","translate("+L+",0)"),d&&(s.width(L).height(A).margin({left:o.left,top:o.top}).svgContainer(C).xScale(g),_.select(".nv-interactive").call(s)),t.width(L).height(A).color(m.map(function(e,t){return e.color||u(e,t)}).filter(function(e,t){return!m[t].disabled}));var H=P.select(".nv-linesWrap").datum(m.filter(function(e){return!e.disabled}));H.transition().call(t),c&&(n.scale(g).ticks(L/100).tickSize(-A,0),P.select(".nv-x.nv-axis").attr("transform","translate(0,"+y.range()[0]+")"),P.select(".nv-x.nv-axis").transition().call(n)),h&&(r.scale(y).ticks(A/36).tickSize(-L,0),P.select(".nv-y.nv-axis").transition().call(r)),i.dispatch.on("stateChange",function(e){b=e,S.stateChange(b),N.update()}),s.dispatch.on("elementMousemove",function(i){t.clearHighlights();var a,f,l,c=[];m.filter(function(e,t){return e.seriesIndex=t,!e.disabled}).forEach(function(n,r){f=e.interactiveBisect(n.values,i.pointXValue,N.x()),t.highlightPoint(r,f,!0);var s=n.values[f];if(typeof s=="undefined")return;typeof a=="undefined"&&(a=s),typeof l=="undefined"&&(l=N.xScale()(N.x()(s,f))),c.push({key:n.key,value:N.y()(s,f),color:u(n,n.seriesIndex)})});if(c.length>2){var h=N.yScale().invert(i.mouseY),p=Math.abs(N.yScale().domain()[0]-N.yScale().domain()[1]),d=.03*p,g=e.nearestValueIndex(c.map(function(e){return e.value}),h,d);g!==null&&(c[g].highlight=!0)}var y=n.tickFormat()(N.x()(a,f));s.tooltip.position({left:l+o.left,top:i.mouseY+o.top}).chartContainer(k.parentNode).enabled(v).valueFormatter(function(e,t){return r.tickFormat()(e)}).data({value:y,series:c})(),s.renderGuideLine(l)}),s.dispatch.on("elementMouseout",function(e){S.tooltipHide(),t.clearHighlights()}),S.on("tooltipShow",function(e){v&&T(e,k.parentNode)}),S.on("changeState",function(e){typeof e.disabled!="undefined"&&m.length===e.disabled.length&&(m.forEach(function(t,n){t.disabled=e.disabled[n]}),b.disabled=e.disabled),N.update()})}),N}var t=e.models.line(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.interactiveGuideline(),o={top:30,right:20,bottom:50,left:60},u=e.utils.defaultColor(),a=null,f=null,l=!0,c=!0,h=!0,p=!1,d=!1,v=!0,m=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" at "+t+"

    "},g,y,b={},w=null,E="No Data Available.",S=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),x=250;n.orient("bottom").tickPadding(7),r.orient(p?"right":"left");var T=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=m(i.series.key,a,f,i,N);e.tooltip.show([o,u],l,null,null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+o.left,e.pos[1]+o.top],S.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){S.tooltipHide(e)}),S.on("tooltipHide",function(){v&&e.tooltip.cleanup()}),N.dispatch=S,N.lines=t,N.legend=i,N.xAxis=n,N.yAxis=r,N.interactiveLayer=s,d3.rebind(N,t,"defined","isArea","x","y","size","xScale","yScale","xDomain","yDomain","xRange","yRange","forceX","forceY","interactive","clipEdge","clipVoronoi","useVoronoi","id","interpolate"),N.options=e.utils.optionsFunc.bind(N),N.margin=function(e){return arguments.length?(o.top=typeof e.top!="undefined"?e.top:o.top,o.right=typeof e.right!="undefined"?e.right:o.right,o.bottom=typeof e.bottom!="undefined"?e.bottom:o.bottom,o.left=typeof e.left!="undefined"?e.left:o.left,N):o},N.width=function(e){return arguments.length?(a=e,N):a},N.height=function(e){return arguments.length?(f=e,N):f},N.color=function(t){return arguments.length?(u=e.utils.getColor(t),i.color(u),N):u},N.showLegend=function(e){return arguments.length?(l=e,N):l},N.showXAxis=function(e){return arguments.length?(c=e,N):c},N.showYAxis=function(e){return arguments.length?(h=e,N):h},N.rightAlignYAxis=function(e){return arguments.length?(p=e,r.orient(e?"right":"left"),N):p},N.useInteractiveGuideline=function(e){return arguments.length?(d=e,e===!0&&(N.interactive(!1),N.useVoronoi(!1)),N):d},N.tooltips=function(e){return arguments.length?(v=e,N):v},N.tooltipContent=function(e){return arguments.length?(m=e,N):m},N.state=function(e){return arguments.length?(b=e,N):b},N.defaultState=function(e){return arguments.length?(w=e,N):w},N.noData=function(e){return arguments.length?(E=e,N):E},N.transitionDuration=function(e){return arguments.length?(x=e,N):x},N},e.models.linePlusBarChart=function(){"use strict";function T(e){return e.each(function(e){var l=d3.select(this),c=this,v=(a||parseInt(l.style("width"))||960)-u.left-u.right,N=(f||parseInt(l.style("height"))||400)-u.top-u.bottom;T.update=function(){l.transition().call(T)},b.disabled=e.map(function(e){return!!e.disabled});if(!w){var C;w={};for(C in b)b[C]instanceof Array?w[C]=b[C].slice(0):w[C]=b[C]}if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var k=l.selectAll(".nv-noData").data([E]);return k.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),k.attr("x",u.left+v/2).attr("y",u.top+N/2).text(function(e){return e}),T}l.selectAll(".nv-noData").remove();var L=e.filter(function(e){return!e.disabled&&e.bar}),A=e.filter(function(e){return!e.bar});m=A.filter(function(e){return!e.disabled}).length&&A.filter(function(e){return!e.disabled})[0].values.length?t.xScale():n.xScale(),g=n.yScale(),y=t.yScale();var O=d3.select(this).selectAll("g.nv-wrap.nv-linePlusBar").data([e]),M=O.enter().append("g").attr("class","nvd3 nv-wrap nv-linePlusBar").append("g"),_=O.select("g");M.append("g").attr("class","nv-x nv-axis"),M.append("g").attr("class","nv-y1 nv-axis"),M.append("g").attr("class","nv-y2 nv-axis"),M.append("g").attr("class","nv-barsWrap"),M.append("g").attr("class","nv-linesWrap"),M.append("g").attr("class","nv-legendWrap"),p&&(o.width(v/2),_.select(".nv-legendWrap").datum(e.map(function(e){return e.originalKey=e.originalKey===undefined?e.key:e.originalKey,e.key=e.originalKey+(e.bar?" (left axis)":" (right axis)"),e})).call(o),u.top!=o.height()&&(u.top=o.height(),N=(f||parseInt(l.style("height"))||400)-u.top-u.bottom),_.select(".nv-legendWrap").attr("transform","translate("+v/2+","+ -u.top+")")),O.attr("transform","translate("+u.left+","+u.top+")"),t.width(v).height(N).color(e.map(function(e,t){return e.color||h(e,t)}).filter(function(t,n){return!e[n].disabled&&!e[n].bar})),n.width(v).height(N).color(e.map(function(e,t){return e.color||h(e,t)}).filter(function(t,n){return!e[n].disabled&&e[n].bar}));var D=_.select(".nv-barsWrap").datum(L.length?L:[{values:[]}]),P=_.select(".nv-linesWrap").datum(A[0]&&!A[0].disabled?A:[{values:[]}]);d3.transition(D).call(n),d3.transition(P).call(t),r.scale(m).ticks(v/100).tickSize(-N,0),_.select(".nv-x.nv-axis").attr("transform","translate(0,"+g.range()[0]+")"),d3.transition(_.select(".nv-x.nv-axis")).call(r),i.scale(g).ticks(N/36).tickSize(-v,0),d3.transition(_.select(".nv-y1.nv-axis")).style("opacity",L.length?1:0).call(i),s.scale(y).ticks(N/36).tickSize(L.length?0:-v,0),_.select(".nv-y2.nv-axis").style("opacity",A.length?1:0).attr("transform","translate("+v+",0)"),d3.transition(_.select(".nv-y2.nv-axis")).call(s),o.dispatch.on("stateChange",function(e){b=e,S.stateChange(b),T.update()}),S.on("tooltipShow",function(e){d&&x(e,c.parentNode)}),S.on("changeState",function(t){typeof t.disabled!="undefined"&&(e.forEach(function(e,n){e.disabled=t.disabled[n]}),b.disabled=t.disabled),T.update()})}),T}var t=e.models.line(),n=e.models.historicalBar(),r=e.models.axis(),i=e.models.axis(),s=e.models.axis(),o=e.models.legend(),u={top:30,right:60,bottom:50,left:60},a=null,f=null,l=function(e){return e.x},c=function(e){return e.y},h=e.utils.defaultColor(),p=!0,d=!0,v=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" at "+t+"

    "},m,g,y,b={},w=null,E="No Data Available.",S=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState");n.padData(!0),t.clipEdge(!1).padData(!0),r.orient("bottom").tickPadding(7).highlightZero(!1),i.orient("left"),s.orient("right");var x=function(n,o){var u=n.pos[0]+(o.offsetLeft||0),a=n.pos[1]+(o.offsetTop||0),f=r.tickFormat()(t.x()(n.point,n.pointIndex)),l=(n.series.bar?i:s).tickFormat()(t.y()(n.point,n.pointIndex)),c=v(n.series.key,f,l,n,T);e.tooltip.show([u,a],c,n.value<0?"n":"s",null,o)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+u.left,e.pos[1]+u.top],S.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){S.tooltipHide(e)}),n.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+u.left,e.pos[1]+u.top],S.tooltipShow(e)}),n.dispatch.on("elementMouseout.tooltip",function(e){S.tooltipHide(e)}),S.on("tooltipHide",function(){d&&e.tooltip.cleanup()}),T.dispatch=S,T.legend=o,T.lines=t,T.bars=n,T.xAxis=r,T.y1Axis=i,T.y2Axis=s,d3.rebind(T,t,"defined","size","clipVoronoi","interpolate"),T.options=e.utils.optionsFunc.bind(T),T.x=function(e){return arguments.length?(l=e,t.x(e),n.x(e),T):l},T.y=function(e){return arguments.length?(c=e,t.y(e),n.y(e),T):c},T.margin=function(e){return arguments.length?(u.top=typeof e.top!="undefined"?e.top:u.top,u.right=typeof e.right!="undefined"?e.right:u.right,u.bottom=typeof e.bottom!="undefined"?e.bottom:u.bottom,u.left=typeof e.left!="undefined"?e.left:u.left,T):u},T.width=function(e){return arguments.length?(a=e,T):a},T.height=function(e){return arguments.length?(f=e,T):f},T.color=function(t){return arguments.length?(h=e.utils.getColor(t),o.color(h),T):h},T.showLegend=function(e){return arguments.length?(p=e,T):p},T.tooltips=function(e){return arguments.length?(d=e,T):d},T.tooltipContent=function(e){return arguments.length?(v=e,T):v},T.state=function(e){return arguments.length?(b=e,T):b},T.defaultState=function(e){return arguments.length?(w=e,T):w},T.noData=function(e){return arguments.length?(E=e,T):E},T},e.models.lineWithFocusChart=function(){"use strict";function k(e){return e.each(function(e){function U(e){var t=+(e=="e"),n=t?1:-1,r=M/3;return"M"+.5*n+","+r+"A6,6 0 0 "+t+" "+6.5*n+","+(r+6)+"V"+(2*r-6)+"A6,6 0 0 "+t+" "+.5*n+","+2*r+"Z"+"M"+2.5*n+","+(r+8)+"V"+(2*r-8)+"M"+4.5*n+","+(r+8)+"V"+(2*r-8)}function z(){a.empty()||a.extent(w),I.data([a.empty()?g.domain():w]).each(function(e,t){var n=g(e[0])-v.range()[0],r=v.range()[1]-g(e[1]);d3.select(this).select(".left").attr("width",n<0?0:n),d3.select(this).select(".right").attr("x",g(e[1])).attr("width",r<0?0:r)})}function W(){w=a.empty()?null:a.extent();var n=a.empty()?g.domain():a.extent();if(Math.abs(n[0]-n[1])<=1)return;T.brush({extent:n,brush:a}),z();var s=H.select(".nv-focus .nv-linesWrap").datum(e.filter(function(e){return!e.disabled}).map(function(e,r){return{key:e.key,values:e.values.filter(function(e,r){return t.x()(e,r)>=n[0]&&t.x()(e,r)<=n[1]})}}));s.transition().duration(N).call(t),H.select(".nv-focus .nv-x.nv-axis").transition().duration(N).call(r),H.select(".nv-focus .nv-y.nv-axis").transition().duration(N).call(i)}var S=d3.select(this),L=this,A=(h||parseInt(S.style("width"))||960)-f.left-f.right,O=(p||parseInt(S.style("height"))||400)-f.top-f.bottom-d,M=d-l.top-l.bottom;k.update=function(){S.transition().duration(N).call(k)},k.container=this;if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var _=S.selectAll(".nv-noData").data([x]);return _.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),_.attr("x",f.left+A/2).attr("y",f.top+O/2).text(function(e){return e}),k}S.selectAll(".nv-noData").remove(),v=t.xScale(),m=t.yScale(),g=n.xScale(),y=n.yScale();var D=S.selectAll("g.nv-wrap.nv-lineWithFocusChart").data([e]),P=D.enter().append("g").attr("class","nvd3 nv-wrap nv-lineWithFocusChart").append("g"),H=D.select("g");P.append("g").attr("class","nv-legendWrap");var B=P.append("g").attr("class","nv-focus");B.append("g").attr("class","nv-x nv-axis"),B.append("g").attr("class","nv-y nv-axis"),B.append("g").attr("class","nv-linesWrap");var j=P.append("g").attr("class","nv-context");j.append("g").attr("class","nv-x nv-axis"),j.append("g").attr("class","nv-y nv-axis"),j.append("g").attr("class","nv-linesWrap"),j.append("g").attr("class","nv-brushBackground"),j.append("g").attr("class","nv-x nv-brush"),b&&(u.width(A),H.select(".nv-legendWrap").datum(e).call(u),f.top!=u.height()&&(f.top=u.height(),O=(p||parseInt(S.style("height"))||400)-f.top-f.bottom-d),H.select(".nv-legendWrap").attr("transform","translate(0,"+ -f.top+")")),D.attr("transform","translate("+f.left+","+f.top+")"),t.width(A).height(O).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),n.defined(t.defined()).width(A).height(M).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),H.select(".nv-context").attr("transform","translate(0,"+(O+f.bottom+l.top)+")");var F=H.select(".nv-context .nv-linesWrap").datum(e.filter(function(e){return!e.disabled}));d3.transition(F).call(n),r.scale(v).ticks(A/100).tickSize(-O,0),i.scale(m).ticks(O/36).tickSize(-A,0),H.select(".nv-focus .nv-x.nv-axis").attr("transform","translate(0,"+O+")"),a.x(g).on("brush",function(){var e=k.transitionDuration();k.transitionDuration(0),W(),k.transitionDuration(e)}),w&&a.extent(w);var I=H.select(".nv-brushBackground").selectAll("g").data([w||a.extent()]),q=I.enter().append("g");q.append("rect").attr("class","left").attr("x",0).attr("y",0).attr("height",M),q.append("rect").attr("class","right").attr("x",0).attr("y",0).attr("height",M);var R=H.select(".nv-x.nv-brush").call(a);R.selectAll("rect").attr("height",M),R.selectAll(".resize").append("path").attr("d",U),W(),s.scale(g).ticks(A/100).tickSize(-M,0),H.select(".nv-context .nv-x.nv-axis").attr("transform","translate(0,"+y.range()[0]+")"),d3.transition(H.select(".nv-context .nv-x.nv-axis")).call(s),o.scale(y).ticks(M/36).tickSize(-A,0),d3.transition(H.select(".nv-context .nv-y.nv-axis")).call(o),H.select(".nv-context .nv-x.nv-axis").attr("transform","translate(0,"+y.range()[0]+")"),u.dispatch.on("stateChange",function(e){k.update()}),T.on("tooltipShow",function(e){E&&C(e,L.parentNode)})}),k}var t=e.models.line(),n=e.models.line(),r=e.models.axis(),i=e.models.axis(),s=e.models.axis(),o=e.models.axis(),u=e.models.legend(),a=d3.svg.brush(),f={top:30,right:30,bottom:30,left:60},l={top:0,right:30,bottom:20,left:60},c=e.utils.defaultColor(),h=null,p=null,d=100,v,m,g,y,b=!0,w=null,E=!0,S=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" at "+t+"

    "},x="No Data Available.",T=d3.dispatch("tooltipShow","tooltipHide","brush"),N=250;t.clipEdge(!0),n.interactive(!1),r.orient("bottom").tickPadding(5),i.orient("left"),s.orient("bottom").tickPadding(5),o.orient("left");var C=function(n,s){var o=n.pos[0]+(s.offsetLeft||0),u=n.pos[1]+(s.offsetTop||0),a=r.tickFormat()(t.x()(n.point,n.pointIndex)),f=i.tickFormat()(t.y()(n.point,n.pointIndex)),l=S(n.series.key,a,f,n,k);e.tooltip.show([o,u],l,null,null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+f.left,e.pos[1]+f.top],T.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),T.on("tooltipHide",function(){E&&e.tooltip.cleanup()}),k.dispatch=T,k.legend=u,k.lines=t,k.lines2=n,k.xAxis=r,k.yAxis=i,k.x2Axis=s,k.y2Axis=o,d3.rebind(k,t,"defined","isArea","size","xDomain","yDomain","xRange","yRange","forceX","forceY","interactive","clipEdge","clipVoronoi","id"),k.options=e.utils.optionsFunc.bind(k),k.x=function(e){return arguments.length?(t.x(e),n.x(e),k):t.x},k.y=function(e){return arguments.length?(t.y(e),n.y(e),k):t.y},k.margin=function(e){return arguments.length?(f.top=typeof e.top!="undefined"?e.top:f.top,f.right=typeof e.right!="undefined"?e.right:f.right,f.bottom=typeof e.bottom!="undefined"?e.bottom:f.bottom,f.left=typeof e.left!="undefined"?e.left:f.left,k):f},k.margin2=function(e){return arguments.length?(l=e,k):l},k.width=function(e){return arguments.length?(h=e,k):h},k.height=function(e){return arguments.length?(p=e,k):p},k.height2=function(e){return arguments.length?(d=e,k):d},k.color=function(t){return arguments.length?(c=e.utils.getColor(t),u.color(c),k):c},k.showLegend=function(e){return arguments.length?(b=e,k):b},k.tooltips=function(e){return arguments.length?(E=e,k):E},k.tooltipContent=function(e){return arguments.length?(S=e,k):S},k.interpolate=function(e){return arguments.length?(t.interpolate(e),n.interpolate(e),k):t.interpolate()},k.noData=function(e){return arguments.length?(x=e,k):x},k.xTickFormat=function(e){return arguments.length?(r.tickFormat(e),s.tickFormat(e),k):r.tickFormat()},k.yTickFormat=function(e){return arguments.length?(i.tickFormat(e),o.tickFormat(e),k):i.tickFormat()},k.brushExtent=function(e){return arguments.length?(w=e,k):w},k.transitionDuration=function(e){return arguments.length?(N=e,k):N},k},e.models.linePlusBarWithFocusChart=function(){"use strict";function B(e){return e.each(function(e){function nt(e){var t=+(e=="e"),n=t?1:-1,r=q/3;return"M"+.5*n+","+r+"A6,6 0 0 "+t+" "+6.5*n+","+(r+6)+"V"+(2*r-6)+"A6,6 0 0 "+t+" "+.5*n+","+2*r+"Z"+"M"+2.5*n+","+(r+8)+"V"+(2*r-8)+"M"+4.5*n+","+(r+8)+"V"+(2*r-8)}function rt(){h.empty()||h.extent(x),Z.data([h.empty()?k.domain():x]).each(function(e,t){var n=k(e[0])-k.range()[0],r=k.range()[1]-k(e[1]);d3.select(this).select(".left").attr("width",n<0?0:n),d3.select(this).select(".right").attr("x",k(e[1])).attr("width",r<0?0:r)})}function it(){x=h.empty()?null:h.extent(),S=h.empty()?k.domain():h.extent(),D.brush({extent:S,brush:h}),rt(),r.width(F).height(I).color(e.map(function(e,t){return e.color||w(e,t)}).filter(function(t,n){return!e[n].disabled&&e[n].bar})),t.width(F).height(I).color(e.map(function(e,t){return e.color||w(e,t)}).filter(function(t,n){return!e[n].disabled&&!e[n].bar}));var n=J.select(".nv-focus .nv-barsWrap").datum(U.length?U.map(function(e,t){return{key:e.key,values:e.values.filter(function(e,t){return r.x()(e,t)>=S[0]&&r.x()(e,t)<=S[1]})}}):[{values:[]}]),i=J.select(".nv-focus .nv-linesWrap").datum(z[0].disabled?[{values:[]}]:z.map(function(e,n){return{key:e.key,values:e.values.filter(function(e,n){return t.x()(e,n)>=S[0]&&t.x()(e,n)<=S[1]})}}));U.length?C=r.xScale():C=t.xScale(),s.scale(C).ticks(F/100).tickSize(-I,0),s.domain([Math.ceil(S[0]),Math.floor(S[1])]),J.select(".nv-x.nv-axis").transition().duration(P).call(s),n.transition().duration(P).call(r),i.transition().duration(P).call(t),J.select(".nv-focus .nv-x.nv-axis").attr("transform","translate(0,"+L.range()[0]+")"),u.scale(L).ticks(I/36).tickSize(-F,0),J.select(".nv-focus .nv-y1.nv-axis").style("opacity",U.length?1:0),a.scale(A).ticks(I/36).tickSize(U.length?0:-F,0),J.select(".nv-focus .nv-y2.nv-axis").style("opacity",z.length?1:0).attr("transform","translate("+C.range()[1]+",0)"),J.select(".nv-focus .nv-y1.nv-axis").transition().duration(P).call(u),J.select(".nv-focus .nv-y2.nv-axis").transition().duration(P).call(a)}var N=d3.select(this),j=this,F=(v||parseInt(N.style("width"))||960)-p.left-p.right,I=(m||parseInt(N.style("height"))||400)-p.top-p.bottom-g,q=g-d.top-d.bottom;B.update=function(){N.transition().duration(P).call(B)},B.container=this;if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var R=N.selectAll(".nv-noData").data([_]);return R.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),R.attr("x",p.left+F/2).attr("y",p.top+I/2).text(function(e){return e}),B}N.selectAll(".nv-noData").remove();var U=e.filter(function(e){return!e.disabled&&e.bar}),z=e.filter(function(e){return!e.bar});C=r.xScale(),k=o.scale(),L=r.yScale(),A=t.yScale(),O=i.yScale(),M=n.yScale();var W=e.filter(function(e){return!e.disabled&&e.bar}).map(function(e){return e.values.map(function(e,t){return{x:y(e,t),y:b(e,t)}})}),X=e.filter(function(e){return!e.disabled&&!e.bar}).map(function(e){return e.values.map(function(e,t){return{x:y(e,t),y:b(e,t)}})});C.range([0,F]),k.domain(d3.extent(d3.merge(W.concat(X)),function(e){return e.x})).range([0,F]);var V=N.selectAll("g.nv-wrap.nv-linePlusBar").data([e]),$=V.enter().append("g").attr("class","nvd3 nv-wrap nv-linePlusBar").append("g"),J=V.select("g");$.append("g").attr("class","nv-legendWrap");var K=$.append("g").attr("class","nv-focus");K.append("g").attr("class","nv-x nv-axis"),K.append("g").attr("class","nv-y1 nv-axis"),K.append("g").attr("class","nv-y2 nv-axis"),K.append("g").attr("class","nv-barsWrap"),K.append("g").attr("class","nv-linesWrap");var Q=$.append("g").attr("class","nv-context");Q.append("g").attr("class","nv-x nv-axis"),Q.append("g").attr("class","nv-y1 nv-axis"),Q.append("g").attr("class","nv-y2 nv-axis"),Q.append("g").attr("class","nv-barsWrap"),Q.append("g").attr("class","nv-linesWrap"),Q.append("g").attr("class","nv-brushBackground"),Q.append("g").attr("class","nv-x nv-brush"),E&&(c.width(F/2),J.select(".nv-legendWrap").datum(e.map(function(e){return e.originalKey=e.originalKey===undefined?e.key:e.originalKey,e.key=e.originalKey+(e.bar?" (left axis)":" (right axis)"),e})).call(c),p.top!=c.height()&&(p.top=c.height(),I=(m||parseInt(N.style("height"))||400)-p.top-p.bottom-g),J.select(".nv-legendWrap").attr("transform","translate("+F/2+","+ -p.top+")")),V.attr("transform","translate("+p.left+","+p.top+")"),i.width(F).height(q).color(e.map(function(e,t){return e.color||w(e,t)}).filter(function(t,n){return!e[n].disabled&&e[n].bar})),n.width(F).height(q).color(e.map(function(e,t){return e.color||w(e,t)}).filter(function(t,n){return!e[n].disabled&&!e[n].bar}));var G=J.select(".nv-context .nv-barsWrap").datum(U.length?U:[{values:[]}]),Y=J.select(".nv-context .nv-linesWrap").datum(z[0].disabled?[{values:[]}]:z);J.select(".nv-context").attr("transform","translate(0,"+(I+p.bottom+d.top)+")"),G.transition().call(i),Y.transition().call(n),h.x(k).on("brush",it),x&&h.extent(x);var Z=J.select(".nv-brushBackground").selectAll("g").data([x||h.extent()]),et=Z.enter().append("g");et.append("rect").attr("class","left").attr("x",0).attr("y",0).attr("height",q),et.append("rect").attr("class","right").attr("x",0).attr("y",0).attr("height",q);var tt=J.select(".nv-x.nv-brush").call(h);tt.selectAll("rect").attr("height",q),tt.selectAll(".resize").append("path").attr("d",nt),o.ticks(F/100).tickSize(-q,0),J.select(".nv-context .nv-x.nv-axis").attr("transform","translate(0,"+O.range()[0]+")"),J.select(".nv-context .nv-x.nv-axis").transition().call(o),f.scale(O).ticks(q/36).tickSize(-F,0),J.select(".nv-context .nv-y1.nv-axis").style("opacity",U.length?1:0).attr("transform","translate(0,"+k.range()[0]+")"),J.select(".nv-context .nv-y1.nv-axis").transition().call(f),l.scale(M).ticks(q/36).tickSize(U.length?0:-F,0),J.select(".nv-context .nv-y2.nv-axis").style("opacity",z.length?1:0).attr("transform","translate("+k.range()[1]+",0)"),J.select(".nv-context .nv-y2.nv-axis").transition().call(l),c.dispatch.on("stateChange",function(e){B.update()}),D.on("tooltipShow",function(e){T&&H(e,j.parentNode)}),it()}),B}var t=e.models.line(),n=e.models.line(),r=e.models.historicalBar(),i=e.models.historicalBar(),s=e.models.axis(),o=e.models.axis(),u=e.models.axis(),a=e.models.axis(),f=e.models.axis(),l=e.models.axis(),c=e.models.legend(),h=d3.svg.brush(),p={top:30,right:30,bottom:30,left:60},d={top:0,right:30,bottom:20,left:60},v=null,m=null,g=100,y=function(e){return e.x},b=function(e){return e.y},w=e.utils.defaultColor(),E=!0,S,x=null,T=!0,N=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" at "+t+"

    "},C,k,L,A,O,M,_="No Data Available.",D=d3.dispatch("tooltipShow","tooltipHide","brush"),P=0;t.clipEdge(!0),n.interactive(!1),s.orient("bottom").tickPadding(5),u.orient("left"),a.orient("right"),o.orient("bottom").tickPadding(5),f.orient("left"),l.orient("right");var H=function(n,r){S&&(n.pointIndex+=Math.ceil(S[0]));var i=n.pos[0]+(r.offsetLeft||0),o=n.pos[1]+(r.offsetTop||0),f=s.tickFormat()(t.x()(n.point,n.pointIndex)),l=(n.series.bar?u:a).tickFormat()(t.y()(n.point,n.pointIndex)),c=N(n.series.key,f,l,n,B);e.tooltip.show([i,o],c,n.value<0?"n":"s",null,r)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+p.left,e.pos[1]+p.top],D.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){D.tooltipHide(e)}),r.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+p.left,e.pos[1]+p.top],D.tooltipShow(e)}),r.dispatch.on("elementMouseout.tooltip",function(e){D.tooltipHide(e)}),D.on("tooltipHide",function(){T&&e.tooltip.cleanup()}),B.dispatch=D,B.legend=c,B.lines=t,B.lines2=n,B.bars=r,B.bars2=i,B.xAxis=s,B.x2Axis=o,B.y1Axis=u,B.y2Axis=a,B.y3Axis=f,B.y4Axis=l,d3.rebind(B,t,"defined","size","clipVoronoi","interpolate"),B.options=e.utils.optionsFunc.bind(B),B.x=function(e){return arguments.length?(y=e,t.x(e),r.x(e),B):y},B.y=function(e){return arguments.length?(b=e,t.y(e),r.y(e),B):b},B.margin=function(e){return arguments.length?(p.top=typeof e.top!="undefined"?e.top:p.top,p.right=typeof e.right!="undefined"?e.right:p.right,p.bottom=typeof e.bottom!="undefined"?e.bottom:p.bottom,p.left=typeof e.left!="undefined"?e.left:p.left,B):p},B.width=function(e){return arguments.length?(v=e,B):v},B.height=function(e){return arguments.length?(m=e,B):m},B.color=function(t){return arguments.length?(w=e.utils.getColor(t),c.color(w),B):w},B.showLegend=function(e){return arguments.length?(E=e,B):E},B.tooltips=function(e){return arguments.length?(T=e,B):T},B.tooltipContent=function(e){return arguments.length?(N=e,B):N},B.noData=function(e){return arguments.length?(_=e,B):_},B.brushExtent=function(e){return arguments.length?(x=e,B):x},B},e.models.multiBar=function(){"use strict";function C(e){return e.each(function(e){var C=n-t.left-t.right,k=r-t.top-t.bottom,L=d3.select(this);d&&e.length&&(d=[{values:e[0].values.map(function(e){return{x:e.x,y:0,series:e.series,size:.01}})}]),c&&(e=d3.layout.stack().offset(h).values(function(e){return e.values}).y(a)(!e.length&&d?d:e)),e.forEach(function(e,t){e.values.forEach(function(e){e.series=t})}),c&&e[0].values.map(function(t,n){var r=0,i=0;e.map(function(e){var t=e.values[n];t.size=Math.abs(t.y),t.y<0?(t.y1=i,i-=t.size):(t.y1=t.size+r,r+=t.size)})});var A=y&&b?[]:e.map(function(e){return e.values.map(function(e,t){return{x:u(e,t),y:a(e,t),y0:e.y0,y1:e.y1}})});i.domain(y||d3.merge(A).map(function(e){return e.x})).rangeBands(w||[0,C],S),s.domain(b||d3.extent(d3.merge(A).map(function(e){return c?e.y>0?e.y1:e.y1+e.y:e.y}).concat(f))).range(E||[k,0]),i.domain()[0]===i.domain()[1]&&(i.domain()[0]?i.domain([i.domain()[0]-i.domain()[0]*.01,i.domain()[1]+i.domain()[1]*.01]):i.domain([-1,1])),s.domain()[0]===s.domain()[1]&&(s.domain()[0]?s.domain([s.domain()[0]+s.domain()[0]*.01,s.domain()[1]-s.domain()[1]*.01]):s.domain([-1,1])),T=T||i,N=N||s;var O=L.selectAll("g.nv-wrap.nv-multibar").data([e]),M=O.enter().append("g").attr("class","nvd3 nv-wrap nv-multibar"),_=M.append("defs"),D=M.append("g"),P=O.select("g");D.append("g").attr("class","nv-groups"),O.attr("transform","translate("+t.left+","+t.top+")"),_.append("clipPath").attr("id","nv-edge-clip-"+o).append("rect"),O.select("#nv-edge-clip-"+o+" rect").attr("width",C).attr("height",k),P.attr("clip-path",l?"url(#nv-edge-clip-"+o+")":"");var H=O.select(".nv-groups").selectAll(".nv-group").data(function(e){return e},function(e,t){return t});H.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),H.exit().transition().selectAll("rect.nv-bar").delay(function(t,n){return n*g/e[0].values.length}).attr("y",function(e){return c?N(e.y0):N(0)}).attr("height",0).remove(),H.attr("class",function(e,t){return"nv-group nv-series-"+t}).classed("hover",function(e){return e.hover}).style("fill",function(e,t){return p(e,t)}).style("stroke",function(e,t){return p(e,t)}),H.transition().style("stroke-opacity",1).style("fill-opacity",.75);var B=H.selectAll("rect.nv-bar").data(function(t){return d&&!e.length?d.values:t.values});B.exit().remove();var j=B.enter().append("rect").attr("class",function(e,t){return a(e,t)<0?"nv-bar negative":"nv-bar positive"}).attr("x",function(t,n,r){return c?0:r*i.rangeBand()/e.length}).attr("y",function(e){return N(c?e.y0:0)}).attr("height",0).attr("width",i.rangeBand()/(c?1:e.length)).attr("transform",function(e,t){return"translate("+i(u(e,t))+",0)"});B.style("fill",function(e,t,n){return p(e,n,t)}).style("stroke",function(e,t,n){return p(e,n,t)}).on("mouseover",function(t,n){d3.select(this).classed("hover",!0),x.elementMouseover({value:a(t,n),point:t,series:e[t.series],pos:[i(u(t,n))+i.rangeBand()*(c?e.length/2:t.series+.5)/e.length,s(a(t,n)+(c?t.y0:0))],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("mouseout",function(t,n){d3.select(this).classed("hover",!1),x.elementMouseout({value:a(t,n),point:t,series:e[t.series],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("click",function(t,n){x.elementClick({value:a(t,n),point:t,series:e[t.series],pos:[i(u(t,n))+i.rangeBand()*(c?e.length/2:t.series+.5)/e.length +,s(a(t,n)+(c?t.y0:0))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()}).on("dblclick",function(t,n){x.elementDblClick({value:a(t,n),point:t,series:e[t.series],pos:[i(u(t,n))+i.rangeBand()*(c?e.length/2:t.series+.5)/e.length,s(a(t,n)+(c?t.y0:0))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()}),B.attr("class",function(e,t){return a(e,t)<0?"nv-bar negative":"nv-bar positive"}).transition().attr("transform",function(e,t){return"translate("+i(u(e,t))+",0)"}),v&&(m||(m=e.map(function(){return!0})),B.style("fill",function(e,t,n){return d3.rgb(v(e,t)).darker(m.map(function(e,t){return t}).filter(function(e,t){return!m[t]})[n]).toString()}).style("stroke",function(e,t,n){return d3.rgb(v(e,t)).darker(m.map(function(e,t){return t}).filter(function(e,t){return!m[t]})[n]).toString()})),c?B.transition().delay(function(t,n){return n*g/e[0].values.length}).attr("y",function(e,t){return s(c?e.y1:0)}).attr("height",function(e,t){return Math.max(Math.abs(s(e.y+(c?e.y0:0))-s(c?e.y0:0)),1)}).attr("x",function(t,n){return c?0:t.series*i.rangeBand()/e.length}).attr("width",i.rangeBand()/(c?1:e.length)):B.transition().delay(function(t,n){return n*g/e[0].values.length}).attr("x",function(t,n){return t.series*i.rangeBand()/e.length}).attr("width",i.rangeBand()/e.length).attr("y",function(e,t){return a(e,t)<0?s(0):s(0)-s(a(e,t))<1?s(0)-1:s(a(e,t))||0}).attr("height",function(e,t){return Math.max(Math.abs(s(a(e,t))-s(0)),1)||0}),T=i.copy(),N=s.copy()}),C}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=d3.scale.ordinal(),s=d3.scale.linear(),o=Math.floor(Math.random()*1e4),u=function(e){return e.x},a=function(e){return e.y},f=[0],l=!0,c=!1,h="zero",p=e.utils.defaultColor(),d=!1,v=null,m,g=1200,y,b,w,E,S=.1,x=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout"),T,N;return C.dispatch=x,C.options=e.utils.optionsFunc.bind(C),C.x=function(e){return arguments.length?(u=e,C):u},C.y=function(e){return arguments.length?(a=e,C):a},C.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,C):t},C.width=function(e){return arguments.length?(n=e,C):n},C.height=function(e){return arguments.length?(r=e,C):r},C.xScale=function(e){return arguments.length?(i=e,C):i},C.yScale=function(e){return arguments.length?(s=e,C):s},C.xDomain=function(e){return arguments.length?(y=e,C):y},C.yDomain=function(e){return arguments.length?(b=e,C):b},C.xRange=function(e){return arguments.length?(w=e,C):w},C.yRange=function(e){return arguments.length?(E=e,C):E},C.forceY=function(e){return arguments.length?(f=e,C):f},C.stacked=function(e){return arguments.length?(c=e,C):c},C.stackOffset=function(e){return arguments.length?(h=e,C):h},C.clipEdge=function(e){return arguments.length?(l=e,C):l},C.color=function(t){return arguments.length?(p=e.utils.getColor(t),C):p},C.barColor=function(t){return arguments.length?(v=e.utils.getColor(t),C):v},C.disabled=function(e){return arguments.length?(m=e,C):m},C.id=function(e){return arguments.length?(o=e,C):o},C.hideable=function(e){return arguments.length?(d=e,C):d},C.delay=function(e){return arguments.length?(g=e,C):g},C.groupSpacing=function(e){return arguments.length?(S=e,C):S},C},e.models.multiBarChart=function(){"use strict";function A(e){return e.each(function(e){var b=d3.select(this),O=this,M=(u||parseInt(b.style("width"))||960)-o.left-o.right,_=(a||parseInt(b.style("height"))||400)-o.top-o.bottom;A.update=function(){b.transition().duration(k).call(A)},A.container=this,S.disabled=e.map(function(e){return!!e.disabled});if(!x){var D;x={};for(D in S)S[D]instanceof Array?x[D]=S[D].slice(0):x[D]=S[D]}if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var P=b.selectAll(".nv-noData").data([T]);return P.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),P.attr("x",o.left+M/2).attr("y",o.top+_/2).text(function(e){return e}),A}b.selectAll(".nv-noData").remove(),w=t.xScale(),E=t.yScale();var H=b.selectAll("g.nv-wrap.nv-multiBarWithLegend").data([e]),B=H.enter().append("g").attr("class","nvd3 nv-wrap nv-multiBarWithLegend").append("g"),j=H.select("g");B.append("g").attr("class","nv-x nv-axis"),B.append("g").attr("class","nv-y nv-axis"),B.append("g").attr("class","nv-barsWrap"),B.append("g").attr("class","nv-legendWrap"),B.append("g").attr("class","nv-controlsWrap"),c&&(i.width(M-C()),t.barColor()&&e.forEach(function(e,t){e.color=d3.rgb("#ccc").darker(t*1.5).toString()}),j.select(".nv-legendWrap").datum(e).call(i),o.top!=i.height()&&(o.top=i.height(),_=(a||parseInt(b.style("height"))||400)-o.top-o.bottom),j.select(".nv-legendWrap").attr("transform","translate("+C()+","+ -o.top+")"));if(l){var F=[{key:"Grouped",disabled:t.stacked()},{key:"Stacked",disabled:!t.stacked()}];s.width(C()).color(["#444","#444","#444"]),j.select(".nv-controlsWrap").datum(F).attr("transform","translate(0,"+ -o.top+")").call(s)}H.attr("transform","translate("+o.left+","+o.top+")"),d&&j.select(".nv-y.nv-axis").attr("transform","translate("+M+",0)"),t.disabled(e.map(function(e){return e.disabled})).width(M).height(_).color(e.map(function(e,t){return e.color||f(e,t)}).filter(function(t,n){return!e[n].disabled}));var I=j.select(".nv-barsWrap").datum(e.filter(function(e){return!e.disabled}));I.transition().call(t);if(h){n.scale(w).ticks(M/100).tickSize(-_,0),j.select(".nv-x.nv-axis").attr("transform","translate(0,"+E.range()[0]+")"),j.select(".nv-x.nv-axis").transition().call(n);var q=j.select(".nv-x.nv-axis > g").selectAll("g");q.selectAll("line, text").style("opacity",1);if(m){var R=function(e,t){return"translate("+e+","+t+")"},U=5,z=17;q.selectAll("text").attr("transform",function(e,t,n){return R(0,n%2==0?U:z)});var W=d3.selectAll(".nv-x.nv-axis .nv-wrap g g text")[0].length;j.selectAll(".nv-x.nv-axis .nv-axisMaxMin text").attr("transform",function(e,t){return R(0,t===0||W%2!==0?z:U)})}v&&q.filter(function(t,n){return n%Math.ceil(e[0].values.length/(M/100))!==0}).selectAll("text, line").style("opacity",0),g&&q.selectAll(".tick text").attr("transform","rotate("+g+" 0,0)").style("text-anchor",g>0?"start":"end"),j.select(".nv-x.nv-axis").selectAll("g.nv-axisMaxMin text").style("opacity",1)}p&&(r.scale(E).ticks(_/36).tickSize(-M,0),j.select(".nv-y.nv-axis").transition().call(r)),i.dispatch.on("stateChange",function(e){S=e,N.stateChange(S),A.update()}),s.dispatch.on("legendClick",function(e,n){if(!e.disabled)return;F=F.map(function(e){return e.disabled=!0,e}),e.disabled=!1;switch(e.key){case"Grouped":t.stacked(!1);break;case"Stacked":t.stacked(!0)}S.stacked=t.stacked(),N.stateChange(S),A.update()}),N.on("tooltipShow",function(e){y&&L(e,O.parentNode)}),N.on("changeState",function(n){typeof n.disabled!="undefined"&&(e.forEach(function(e,t){e.disabled=n.disabled[t]}),S.disabled=n.disabled),typeof n.stacked!="undefined"&&(t.stacked(n.stacked),S.stacked=n.stacked),A.update()})}),A}var t=e.models.multiBar(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.models.legend(),o={top:30,right:20,bottom:50,left:60},u=null,a=null,f=e.utils.defaultColor(),l=!0,c=!0,h=!0,p=!0,d=!1,v=!0,m=!1,g=0,y=!0,b=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" on "+t+"

    "},w,E,S={stacked:!1},x=null,T="No Data Available.",N=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),C=function(){return l?180:0},k=250;t.stacked(!1),n.orient("bottom").tickPadding(7).highlightZero(!0).showMaxMin(!1).tickFormat(function(e){return e}),r.orient(d?"right":"left").tickFormat(d3.format(",.1f")),s.updateState(!1);var L=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=b(i.series.key,a,f,i,A);e.tooltip.show([o,u],l,i.value<0?"n":"s",null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+o.left,e.pos[1]+o.top],N.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){N.tooltipHide(e)}),N.on("tooltipHide",function(){y&&e.tooltip.cleanup()}),A.dispatch=N,A.multibar=t,A.legend=i,A.xAxis=n,A.yAxis=r,d3.rebind(A,t,"x","y","xDomain","yDomain","xRange","yRange","forceX","forceY","clipEdge","id","stacked","stackOffset","delay","barColor","groupSpacing"),A.options=e.utils.optionsFunc.bind(A),A.margin=function(e){return arguments.length?(o.top=typeof e.top!="undefined"?e.top:o.top,o.right=typeof e.right!="undefined"?e.right:o.right,o.bottom=typeof e.bottom!="undefined"?e.bottom:o.bottom,o.left=typeof e.left!="undefined"?e.left:o.left,A):o},A.width=function(e){return arguments.length?(u=e,A):u},A.height=function(e){return arguments.length?(a=e,A):a},A.color=function(t){return arguments.length?(f=e.utils.getColor(t),i.color(f),A):f},A.showControls=function(e){return arguments.length?(l=e,A):l},A.showLegend=function(e){return arguments.length?(c=e,A):c},A.showXAxis=function(e){return arguments.length?(h=e,A):h},A.showYAxis=function(e){return arguments.length?(p=e,A):p},A.rightAlignYAxis=function(e){return arguments.length?(d=e,r.orient(e?"right":"left"),A):d},A.reduceXTicks=function(e){return arguments.length?(v=e,A):v},A.rotateLabels=function(e){return arguments.length?(g=e,A):g},A.staggerLabels=function(e){return arguments.length?(m=e,A):m},A.tooltip=function(e){return arguments.length?(b=e,A):b},A.tooltips=function(e){return arguments.length?(y=e,A):y},A.tooltipContent=function(e){return arguments.length?(b=e,A):b},A.state=function(e){return arguments.length?(S=e,A):S},A.defaultState=function(e){return arguments.length?(x=e,A):x},A.noData=function(e){return arguments.length?(T=e,A):T},A.transitionDuration=function(e){return arguments.length?(k=e,A):k},A},e.models.multiBarHorizontal=function(){"use strict";function C(e){return e.each(function(e){var i=n-t.left-t.right,y=r-t.top-t.bottom,C=d3.select(this);p&&(e=d3.layout.stack().offset("zero").values(function(e){return e.values}).y(a)(e)),e.forEach(function(e,t){e.values.forEach(function(e){e.series=t})}),p&&e[0].values.map(function(t,n){var r=0,i=0;e.map(function(e){var t=e.values[n];t.size=Math.abs(t.y),t.y<0?(t.y1=i-t.size,i-=t.size):(t.y1=r,r+=t.size)})});var k=b&&w?[]:e.map(function(e){return e.values.map(function(e,t){return{x:u(e,t),y:a(e,t),y0:e.y0,y1:e.y1}})});s.domain(b||d3.merge(k).map(function(e){return e.x})).rangeBands(E||[0,y],.1),o.domain(w||d3.extent(d3.merge(k).map(function(e){return p?e.y>0?e.y1+e.y:e.y1:e.y}).concat(f))),d&&!p?o.range(S||[o.domain()[0]<0?m:0,i-(o.domain()[1]>0?m:0)]):o.range(S||[0,i]),T=T||s,N=N||d3.scale.linear().domain(o.domain()).range([o(0),o(0)]);var L=d3.select(this).selectAll("g.nv-wrap.nv-multibarHorizontal").data([e]),A=L.enter().append("g").attr("class","nvd3 nv-wrap nv-multibarHorizontal"),O=A.append("defs"),M=A.append("g"),_=L.select("g");M.append("g").attr("class","nv-groups"),L.attr("transform","translate("+t.left+","+t.top+")");var D=L.select(".nv-groups").selectAll(".nv-group").data(function(e){return e},function(e,t){return t});D.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),D.exit().transition().style("stroke-opacity",1e-6).style("fill-opacity",1e-6).remove(),D.attr("class",function(e,t){return"nv-group nv-series-"+t}).classed("hover",function(e){return e.hover}).style("fill",function(e,t){return l(e,t)}).style("stroke",function(e,t){return l(e,t)}),D.transition().style("stroke-opacity",1).style("fill-opacity",.75);var P=D.selectAll("g.nv-bar").data(function(e){return e.values});P.exit().remove();var H=P.enter().append("g").attr("transform",function(t,n,r){return"translate("+N(p?t.y0:0)+","+(p?0:r*s.rangeBand()/e.length+s(u(t,n)))+")"});H.append("rect").attr("width",0).attr("height",s.rangeBand()/(p?1:e.length)),P.on("mouseover",function(t,n){d3.select(this).classed("hover",!0),x.elementMouseover({value:a(t,n),point:t,series:e[t.series],pos:[o(a(t,n)+(p?t.y0:0)),s(u(t,n))+s.rangeBand()*(p?e.length/2:t.series+.5)/e.length],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("mouseout",function(t,n){d3.select(this).classed("hover",!1),x.elementMouseout({value:a(t,n),point:t,series:e[t.series],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("click",function(t,n){x.elementClick({value:a(t,n),point:t,series:e[t.series],pos:[s(u(t,n))+s.rangeBand()*(p?e.length/2:t.series+.5)/e.length,o(a(t,n)+(p?t.y0:0))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()}).on("dblclick",function(t,n){x.elementDblClick({value:a(t,n),point:t,series:e[t.series],pos:[s(u(t,n))+s.rangeBand()*(p?e.length/2:t.series+.5)/e.length,o(a(t,n)+(p?t.y0:0))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()}),H.append("text"),d&&!p?(P.select("text").attr("text-anchor",function(e,t){return a(e,t)<0?"end":"start"}).attr("y",s.rangeBand()/(e.length*2)).attr("dy",".32em").text(function(e,t){return g(a(e,t))}),P.transition().select("text").attr("x",function(e,t){return a(e,t)<0?-4:o(a(e,t))-o(0)+4})):P.selectAll("text").text(""),v&&!p?(H.append("text").classed("nv-bar-label",!0),P.select("text.nv-bar-label").attr("text-anchor",function(e,t){return a(e,t)<0?"start":"end"}).attr("y",s.rangeBand()/(e.length*2)).attr("dy",".32em").text(function(e,t){return u(e,t)}),P.transition().select("text.nv-bar-label").attr("x",function(e,t){return a(e,t)<0?o(0)-o(a(e,t))+4:-4})):P.selectAll("text.nv-bar-label").text(""),P.attr("class",function(e,t){return a(e,t)<0?"nv-bar negative":"nv-bar positive"}),c&&(h||(h=e.map(function(){return!0})),P.style("fill",function(e,t,n){return d3.rgb(c(e,t)).darker(h.map(function(e,t){return t}).filter(function(e,t){return!h[t]})[n]).toString()}).style("stroke",function(e,t,n){return d3.rgb(c(e,t)).darker(h.map(function(e,t){return t}).filter(function(e,t){return!h[t]})[n]).toString()})),p?P.transition().attr("transform",function(e,t){return"translate("+o(e.y1)+","+s(u(e,t))+")"}).select("rect").attr("width",function(e,t){return Math.abs(o(a(e,t)+e.y0)-o(e.y0))}).attr("height",s.rangeBand()):P.transition().attr("transform",function(t,n){return"translate("+(a(t,n)<0?o(a(t,n)):o(0))+","+(t.series*s.rangeBand()/e.length+s(u(t,n)))+")"}).select("rect").attr("height",s.rangeBand()/e.length).attr("width",function(e,t){return Math.max(Math.abs(o(a(e,t))-o(0)),1)}),T=s.copy(),N=o.copy()}),C}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=Math.floor(Math.random()*1e4),s=d3.scale.ordinal(),o=d3.scale.linear(),u=function(e){return e.x},a=function(e){return e.y},f=[0],l=e.utils.defaultColor(),c=null,h,p=!1,d=!1,v=!1,m=60,g=d3.format(",.2f"),y=1200,b,w,E,S,x=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout"),T,N;return C.dispatch=x,C.options=e.utils.optionsFunc.bind(C),C.x=function(e){return arguments.length?(u=e,C):u},C.y=function(e){return arguments.length?(a=e,C):a},C.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,C):t},C.width=function(e){return arguments.length?(n=e,C):n},C.height=function(e){return arguments.length?(r=e,C):r},C.xScale=function(e){return arguments.length?(s=e,C):s},C.yScale=function(e){return arguments.length?(o=e,C):o},C.xDomain=function(e){return arguments.length?(b=e,C):b},C.yDomain=function(e){return arguments.length?(w=e,C):w},C.xRange=function(e){return arguments.length?(E=e,C):E},C.yRange=function(e){return arguments.length?(S=e,C):S},C.forceY=function(e){return arguments.length?(f=e,C):f},C.stacked=function(e){return arguments.length?(p=e,C):p},C.color=function(t){return arguments.length?(l=e.utils.getColor(t),C):l},C.barColor=function(t){return arguments.length?(c=e.utils.getColor(t),C):c},C.disabled=function(e){return arguments.length?(h=e,C):h},C.id=function(e){return arguments.length?(i=e,C):i},C.delay=function(e){return arguments.length?(y=e,C):y},C.showValues=function(e){return arguments.length?(d=e,C):d},C.showBarLabels=function(e){return arguments.length?(v=e,C):v},C.valueFormat=function(e){return arguments.length?(g=e,C):g},C.valuePadding=function(e){return arguments.length?(m=e,C):m},C},e.models.multiBarHorizontalChart=function(){"use strict";function C(e){return e.each(function(e){var d=d3.select(this),m=this,k=(u||parseInt(d.style("width"))||960)-o.left-o.right,L=(a||parseInt(d.style("height"))||400)-o.top-o.bottom;C.update=function(){d.transition().duration(T).call(C)},C.container=this,b.disabled=e.map(function(e){return!!e.disabled});if(!w){var A;w={};for(A in b)b[A]instanceof Array?w[A]=b[A].slice(0):w[A]=b[A]}if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var O=d.selectAll(".nv-noData").data([E]);return O.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),O.attr("x",o.left+k/2).attr("y",o.top+L/2).text(function(e){return e}),C}d.selectAll(".nv-noData").remove(),g=t.xScale(),y=t.yScale();var M=d.selectAll("g.nv-wrap.nv-multiBarHorizontalChart").data([e]),_=M.enter().append("g").attr("class","nvd3 nv-wrap nv-multiBarHorizontalChart").append("g"),D=M.select("g");_.append("g").attr("class","nv-x nv-axis"),_.append("g").attr("class","nv-y nv-axis").append("g").attr("class","nv-zeroLine").append("line"),_.append("g").attr("class","nv-barsWrap"),_.append("g").attr("class","nv-legendWrap"),_.append("g").attr("class","nv-controlsWrap"),c&&(i.width(k-x()),t.barColor()&&e.forEach(function(e,t){e.color=d3.rgb("#ccc").darker(t*1.5).toString()}),D.select(".nv-legendWrap").datum(e).call(i),o.top!=i.height()&&(o.top=i.height(),L=(a||parseInt(d.style("height"))||400)-o.top-o.bottom),D.select(".nv-legendWrap").attr("transform","translate("+x()+","+ -o.top+")"));if(l){var P=[{key:"Grouped",disabled:t.stacked()},{key:"Stacked",disabled:!t.stacked()}];s.width(x()).color(["#444","#444","#444"]),D.select(".nv-controlsWrap").datum(P).attr("transform","translate(0,"+ -o.top+")").call(s)}M.attr("transform","translate("+o.left+","+o.top+")"),t.disabled(e.map(function(e){return e.disabled})).width(k).height(L).color(e.map(function(e,t){return e.color||f(e,t)}).filter(function(t,n){return!e[n].disabled}));var H=D.select(".nv-barsWrap").datum(e.filter(function(e){return!e.disabled}));H.transition().call(t);if(h){n.scale(g).ticks(L/24).tickSize(-k,0),D.select(".nv-x.nv-axis").transition().call(n);var B=D.select(".nv-x.nv-axis").selectAll("g");B.selectAll("line, text")}p&&(r.scale(y).ticks(k/100).tickSize(-L,0),D.select(".nv-y.nv-axis").attr("transform","translate(0,"+L+")"),D.select(".nv-y.nv-axis").transition().call(r)),D.select(".nv-zeroLine line").attr("x1",y(0)).attr("x2",y(0)).attr("y1",0).attr("y2",-L),i.dispatch.on("stateChange",function(e){b=e,S.stateChange(b),C.update()}),s.dispatch.on("legendClick",function(e,n){if(!e.disabled)return;P=P.map(function(e){return e.disabled=!0,e}),e.disabled=!1;switch(e.key){case"Grouped":t.stacked(!1);break;case"Stacked":t.stacked(!0)}b.stacked=t.stacked(),S.stateChange(b),C.update()}),S.on("tooltipShow",function(e){v&&N(e,m.parentNode)}),S.on("changeState",function(n){typeof n.disabled!="undefined"&&(e.forEach(function(e,t){e.disabled=n.disabled[t]}),b.disabled=n.disabled),typeof n.stacked!="undefined"&&(t.stacked(n.stacked),b.stacked=n.stacked),C.update()})}),C}var t=e.models.multiBarHorizontal(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend().height(30),s=e.models.legend().height(30),o={top:30,right:20,bottom:50,left:60},u=null,a=null,f=e.utils.defaultColor(),l=!0,c=!0,h=!0,p=!0,d=!1,v=!0,m=function(e,t,n,r,i){return"

    "+e+" - "+t+"

    "+"

    "+n+"

    "},g,y,b={stacked:d},w=null,E="No Data Available.",S=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),x=function(){return l?180:0},T=250;t.stacked(d),n.orient("left").tickPadding(5).highlightZero(!1).showMaxMin(!1).tickFormat(function(e){return e}),r.orient("bottom").tickFormat(d3.format(",.1f")),s.updateState(!1);var N=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=m(i.series.key,a,f,i,C);e.tooltip.show([o,u],l,i.value<0?"e":"w",null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+o.left,e.pos[1]+o.top],S.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){S.tooltipHide(e)}),S.on("tooltipHide",function(){v&&e.tooltip.cleanup()}),C.dispatch=S,C.multibar=t,C.legend=i,C.xAxis=n,C.yAxis=r,d3.rebind(C,t,"x","y","xDomain","yDomain","xRange","yRange","forceX","forceY","clipEdge","id","delay","showValues","showBarLabels","valueFormat","stacked","barColor"),C.options=e.utils.optionsFunc.bind(C),C.margin=function(e){return arguments.length?(o.top=typeof e.top!="undefined"?e.top:o.top,o.right=typeof e.right!="undefined"?e.right:o.right,o.bottom=typeof e.bottom!="undefined"?e.bottom:o.bottom,o.left=typeof e.left!="undefined"?e.left:o.left,C):o},C.width=function(e){return arguments.length?(u=e,C):u},C.height=function(e){return arguments.length?(a=e,C):a},C.color=function(t){return arguments.length?(f=e.utils.getColor(t),i.color(f),C):f},C.showControls=function(e){return arguments.length?(l=e,C):l},C.showLegend=function(e){return arguments.length?(c=e,C):c},C.showXAxis=function(e){return arguments.length?(h=e,C):h},C.showYAxis=function(e){return arguments.length?(p=e,C):p},C.tooltip=function(e){return arguments.length?(m=e,C):m},C.tooltips=function(e){return arguments.length?(v=e,C):v},C.tooltipContent=function(e){return arguments.length?(m=e,C):m},C.state=function(e){return arguments.length?(b=e,C):b},C.defaultState=function(e){return arguments.length?(w=e,C):w},C.noData=function(e){return arguments.length?(E=e,C):E},C.transitionDuration=function(e){return arguments.length?(T=e,C):T},C},e.models.multiChart=function(){"use strict";function C(e){return e.each(function(e){var u=d3.select(this),f=this;C.update=function(){u.transition().call(C)},C.container=this;var k=(r||parseInt(u.style("width"))||960)-t.left-t.right,L=(i||parseInt(u.style("height"))||400)-t.top-t.bottom,A=e.filter(function(e){return!e.disabled&&e.type=="line"&&e.yAxis==1}),O=e.filter(function(e){return!e.disabled&&e.type=="line"&&e.yAxis==2}),M=e.filter(function(e){return!e.disabled&&e.type=="bar"&&e.yAxis==1}),_=e.filter(function(e){return!e.disabled&&e.type=="bar"&&e.yAxis==2}),D=e.filter(function(e){return!e.disabled&&e.type=="area"&&e.yAxis==1}),P=e.filter(function(e){return!e.disabled&&e.type=="area"&&e.yAxis==2}),H=e.filter(function(e){return!e.disabled&&e.yAxis==1}).map(function(e){return e.values.map(function(e,t){return{x:e.x,y:e.y}})}),B=e.filter(function(e){return!e.disabled&&e.yAxis==2}).map(function(e){return e.values.map(function(e,t){return{x:e.x,y:e.y}})});a.domain(d3.extent(d3.merge(H.concat(B)),function(e){return e.x})).range([0,k]);var j=u.selectAll("g.wrap.multiChart").data([e]),F=j.enter().append("g").attr("class","wrap nvd3 multiChart").append("g");F.append("g").attr("class","x axis"),F.append("g").attr("class","y1 axis"),F.append("g").attr("class","y2 axis"),F.append("g").attr("class","lines1Wrap"),F.append("g").attr("class","lines2Wrap"),F.append("g").attr("class","bars1Wrap"),F.append("g").attr("class","bars2Wrap"),F.append("g").attr("class","stack1Wrap"),F.append("g").attr("class","stack2Wrap"),F.append("g").attr("class","legendWrap");var I=j.select("g");s&&(x.width(k/2),I.select(".legendWrap").datum(e.map(function(e){return e.originalKey=e.originalKey===undefined?e.key:e.originalKey,e.key=e.originalKey+(e.yAxis==1?"":" (right axis)"),e})).call(x),t.top!=x.height()&&(t.top=x.height(),L=(i||parseInt(u.style("height"))||400)-t.top-t.bottom),I.select(".legendWrap").attr("transform","translate("+k/2+","+ -t.top+")")),d.width(k).height(L).interpolate("monotone").color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==1&&e[n].type=="line"})),v.width(k).height(L).interpolate("monotone").color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==2&&e[n].type=="line"})),m.width(k).height(L).color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==1&&e[n].type=="bar"})),g.width(k).height(L).color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==2&&e[n].type=="bar"})),y.width(k).height(L).color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==1&&e[n].type=="area"})),b.width(k).height(L).color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==2&&e[n].type=="area"})),I.attr("transform","translate("+t.left+","+t.top+")");var q=I.select(".lines1Wrap").datum(A),R=I.select(".bars1Wrap").datum(M),U=I.select(".stack1Wrap").datum(D),z=I.select(".lines2Wrap").datum(O),W=I.select(".bars2Wrap").datum(_),X=I.select(".stack2Wrap").datum(P),V=D.length?D.map(function(e){return e.values}).reduce(function(e,t){return e.map(function(e,n){return{x:e.x,y:e.y+t[n].y}})}).concat([{x:0,y:0}]):[],$=P.length?P.map(function(e){return e.values}).reduce(function(e,t){return e.map(function(e,n){return{x:e.x,y:e.y+t[n].y}})}).concat([{x:0,y:0}]):[];h.domain(l||d3.extent(d3.merge(H).concat(V),function(e){return e.y})).range([0,L]),p.domain(c||d3.extent(d3.merge(B).concat($),function(e){return e.y})).range([0,L]),d.yDomain(h.domain()),m.yDomain(h.domain()),y.yDomain(h.domain()),v.yDomain(p.domain()),g.yDomain(p.domain()),b.yDomain(p.domain()),D.length&&d3.transition(U).call(y),P.length&&d3.transition(X).call(b),M.length&&d3.transition(R).call(m),_.length&&d3.transition(W).call(g),A.length&&d3.transition(q).call(d),O.length&&d3.transition(z).call(v),w.ticks(k/100).tickSize(-L,0),I.select(".x.axis").attr("transform","translate(0,"+L+")"),d3.transition(I.select(".x.axis")).call(w),E.ticks(L/36).tickSize(-k,0),d3.transition(I.select(".y1.axis")).call(E),S.ticks(L/36).tickSize(-k,0),d3.transition(I.select(".y2.axis")).call(S),I.select(".y2.axis").style("opacity",B.length?1:0).attr("transform","translate("+a.range()[1]+",0)"),x.dispatch.on("stateChange",function(e){C.update()}),T.on("tooltipShow",function(e){o&&N(e,f.parentNode)})}),C}var t={top:30,right:20,bottom:50,left:60},n=d3.scale.category20().range(),r=null,i=null,s=!0,o=!0,u=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" at "+t+"

    "},a,f,l,c,a=d3.scale.linear(),h=d3.scale.linear(),p=d3.scale.linear(),d=e.models.line().yScale(h),v=e.models.line().yScale(p),m=e.models.multiBar().stacked(!1).yScale(h),g=e.models.multiBar().stacked(!1).yScale(p),y=e.models.stackedArea().yScale(h),b=e.models.stackedArea().yScale(p),w=e.models.axis().scale(a).orient("bottom").tickPadding(5),E=e.models.axis().scale(h).orient("left"),S=e.models.axis().scale(p).orient("right"),x=e.models.legend().height(30),T=d3.dispatch("tooltipShow","tooltipHide"),N=function(t,n){var r=t.pos[0]+(n.offsetLeft||0),i=t.pos[1]+(n.offsetTop||0),s=w.tickFormat()(d.x()(t.point,t.pointIndex)),o=(t.series.yAxis==2?S:E).tickFormat()(d.y()(t.point,t.pointIndex)),a=u(t.series.key,s,o,t,C);e.tooltip.show([r,i],a,undefined,undefined,n.offsetParent)};return d.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),d.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),v.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),v.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),m.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),m.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),g.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),g.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),y.dispatch.on("tooltipShow",function(e){if(!Math.round(y.y()(e.point)*100))return setTimeout(function(){d3.selectAll(".point.hover").classed("hover",!1)},0),!1;e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),y.dispatch.on("tooltipHide",function(e){T.tooltipHide(e)}),b.dispatch.on("tooltipShow",function(e){if(!Math.round(b.y()(e.point)*100))return setTimeout(function(){d3.selectAll(".point.hover").classed("hover",!1)},0),!1;e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),b.dispatch.on("tooltipHide",function(e){T.tooltipHide(e)}),d.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),d.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),v.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),v.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),T.on("tooltipHide",function(){o&&e.tooltip.cleanup()}),C.dispatch=T,C.lines1=d,C.lines2=v,C.bars1=m,C.bars2=g,C.stack1=y,C.stack2=b,C.xAxis=w,C.yAxis1=E,C.yAxis2=S,C.options=e.utils.optionsFunc.bind(C),C.x=function(e){return arguments.length?(getX=e,d.x(e),m.x(e),C):getX},C.y=function(e){return arguments.length?(getY=e,d.y(e),m.y(e),C):getY},C.yDomain1=function(e){return arguments.length?(l=e,C):l},C.yDomain2=function(e){return arguments.length?(c=e,C):c},C.margin=function(e){return arguments.length?(t=e,C):t},C.width=function(e){return arguments.length?(r=e,C):r},C.height=function(e){return arguments.length?(i=e,C):i},C.color=function(e){return arguments.length?(n=e,x.color(e),C):n},C.showLegend=function(e){return arguments.length?(s=e,C):s},C.tooltips=function(e){return arguments.length?(o=e,C):o},C.tooltipContent=function(e){return arguments.length?(u=e,C):u},C},e.models.ohlcBar=function(){"use strict";function x(e){return e.each(function(e){var g=n-t.left-t.right,x=r-t.top-t.bottom,T=d3.select(this);s.domain(y||d3.extent(e[0].values.map(u).concat(p))),v?s.range(w||[g*.5/e[0].values.length,g*(e[0].values.length-.5)/e[0].values.length]):s.range(w||[0,g]),o.domain(b||[d3.min(e[0].values.map(h).concat(d)),d3.max(e[0].values.map(c).concat(d))]).range(E||[x,0]),s.domain()[0]===s.domain()[1]&&(s.domain()[0]?s.domain([s.domain()[0]-s.domain()[0]*.01,s.domain()[1]+s.domain()[1]*.01]):s.domain([-1,1])),o.domain()[0]===o.domain()[1]&&(o.domain()[0]?o.domain([o.domain()[0]+o.domain()[0]*.01,o.domain()[1]-o.domain()[1]*.01]):o.domain([-1,1]));var N=d3.select(this).selectAll("g.nv-wrap.nv-ohlcBar").data([e[0].values]),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-ohlcBar"),k=C.append("defs"),L=C.append("g"),A=N.select("g");L.append("g").attr("class","nv-ticks"),N.attr("transform","translate("+t.left+","+t.top+")"),T.on("click",function(e,t){S.chartClick({data:e,index:t,pos:d3.event,id:i})}),k.append("clipPath").attr("id","nv-chart-clip-path-"+i).append("rect"),N.select("#nv-chart-clip-path-"+i+" rect").attr("width",g).attr("height",x),A.attr("clip-path",m?"url(#nv-chart-clip-path-"+i+")":"");var O=N.select(".nv-ticks").selectAll(".nv-tick").data(function(e){return e});O.exit().remove();var M=O.enter().append("path").attr("class",function(e,t,n){return(f(e,t)>l(e,t)?"nv-tick negative":"nv-tick positive")+" nv-tick-"+n+"-"+t}).attr("d",function(t,n){var r=g/e[0].values.length*.9;return"m0,0l0,"+(o(f(t,n))-o(c(t,n)))+"l"+ -r/2+",0l"+r/2+",0l0,"+(o(h(t,n))-o(f(t,n)))+"l0,"+(o(l(t,n))-o(h(t,n)))+"l"+r/2+",0l"+ -r/2+",0z"}).attr("transform",function(e,t){return"translate("+s(u(e,t))+","+o(c(e,t))+")"}).on("mouseover",function(t,n){d3.select(this).classed("hover",!0),S.elementMouseover({point:t,series:e[0],pos:[s(u(t,n)),o(a(t,n))],pointIndex:n,seriesIndex:0,e:d3.event})}).on("mouseout",function(t,n){d3.select(this).classed("hover",!1),S.elementMouseout({point:t,series:e[0],pointIndex:n,seriesIndex:0,e:d3.event})}).on("click",function(e,t){S.elementClick({value:a(e,t),data:e,index:t,pos:[s(u(e,t)),o(a(e,t))],e:d3.event,id:i}),d3.event.stopPropagation()}).on("dblclick",function(e,t){S.elementDblClick({value:a(e,t),data:e,index:t,pos:[s(u(e,t)),o(a(e,t))],e:d3.event,id:i}),d3.event.stopPropagation()});O.attr("class",function(e,t,n){return(f(e,t)>l(e,t)?"nv-tick negative":"nv-tick positive")+" nv-tick-"+n+"-"+t}),d3.transition(O).attr("transform",function(e,t){return"translate("+s(u(e,t))+","+o(c(e,t))+")"}).attr("d",function(t,n){var r=g/e[0].values.length*.9;return"m0,0l0,"+(o(f(t,n))-o(c(t,n)))+"l"+ -r/2+",0l"+r/2+",0l0,"+(o(h(t,n))-o(f(t,n)))+"l0,"+(o(l(t,n))-o(h(t,n)))+"l"+r/2+",0l"+ -r/2+",0z"})}),x}var t={top:0 +,right:0,bottom:0,left:0},n=960,r=500,i=Math.floor(Math.random()*1e4),s=d3.scale.linear(),o=d3.scale.linear(),u=function(e){return e.x},a=function(e){return e.y},f=function(e){return e.open},l=function(e){return e.close},c=function(e){return e.high},h=function(e){return e.low},p=[],d=[],v=!1,m=!0,g=e.utils.defaultColor(),y,b,w,E,S=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout");return x.dispatch=S,x.options=e.utils.optionsFunc.bind(x),x.x=function(e){return arguments.length?(u=e,x):u},x.y=function(e){return arguments.length?(a=e,x):a},x.open=function(e){return arguments.length?(f=e,x):f},x.close=function(e){return arguments.length?(l=e,x):l},x.high=function(e){return arguments.length?(c=e,x):c},x.low=function(e){return arguments.length?(h=e,x):h},x.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,x):t},x.width=function(e){return arguments.length?(n=e,x):n},x.height=function(e){return arguments.length?(r=e,x):r},x.xScale=function(e){return arguments.length?(s=e,x):s},x.yScale=function(e){return arguments.length?(o=e,x):o},x.xDomain=function(e){return arguments.length?(y=e,x):y},x.yDomain=function(e){return arguments.length?(b=e,x):b},x.xRange=function(e){return arguments.length?(w=e,x):w},x.yRange=function(e){return arguments.length?(E=e,x):E},x.forceX=function(e){return arguments.length?(p=e,x):p},x.forceY=function(e){return arguments.length?(d=e,x):d},x.padData=function(e){return arguments.length?(v=e,x):v},x.clipEdge=function(e){return arguments.length?(m=e,x):m},x.color=function(t){return arguments.length?(g=e.utils.getColor(t),x):g},x.id=function(e){return arguments.length?(i=e,x):i},x},e.models.pie=function(){"use strict";function S(e){return e.each(function(e){function q(e){var t=(e.startAngle+e.endAngle)*90/Math.PI-90;return t>90?t-180:t}function R(e){e.endAngle=isNaN(e.endAngle)?0:e.endAngle,e.startAngle=isNaN(e.startAngle)?0:e.startAngle,m||(e.innerRadius=0);var t=d3.interpolate(this._current,e);return this._current=t(0),function(e){return A(t(e))}}function U(e){e.innerRadius=0;var t=d3.interpolate({startAngle:0,endAngle:0},e);return function(e){return A(t(e))}}var o=n-t.left-t.right,f=r-t.top-t.bottom,S=Math.min(o,f)/2,x=S-S/5,T=d3.select(this),N=T.selectAll(".nv-wrap.nv-pie").data(e),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-pie nv-chart-"+u),k=C.append("g"),L=N.select("g");k.append("g").attr("class","nv-pie"),k.append("g").attr("class","nv-pieLabels"),N.attr("transform","translate("+t.left+","+t.top+")"),L.select(".nv-pie").attr("transform","translate("+o/2+","+f/2+")"),L.select(".nv-pieLabels").attr("transform","translate("+o/2+","+f/2+")"),T.on("click",function(e,t){E.chartClick({data:e,index:t,pos:d3.event,id:u})});var A=d3.svg.arc().outerRadius(x);y&&A.startAngle(y),b&&A.endAngle(b),m&&A.innerRadius(S*w);var O=d3.layout.pie().sort(null).value(function(e){return e.disabled?0:s(e)}),M=N.select(".nv-pie").selectAll(".nv-slice").data(O),_=N.select(".nv-pieLabels").selectAll(".nv-label").data(O);M.exit().remove(),_.exit().remove();var D=M.enter().append("g").attr("class","nv-slice").on("mouseover",function(e,t){d3.select(this).classed("hover",!0),E.elementMouseover({label:i(e.data),value:s(e.data),point:e.data,pointIndex:t,pos:[d3.event.pageX,d3.event.pageY],id:u})}).on("mouseout",function(e,t){d3.select(this).classed("hover",!1),E.elementMouseout({label:i(e.data),value:s(e.data),point:e.data,index:t,id:u})}).on("click",function(e,t){E.elementClick({label:i(e.data),value:s(e.data),point:e.data,index:t,pos:d3.event,id:u}),d3.event.stopPropagation()}).on("dblclick",function(e,t){E.elementDblClick({label:i(e.data),value:s(e.data),point:e.data,index:t,pos:d3.event,id:u}),d3.event.stopPropagation()});M.attr("fill",function(e,t){return a(e,t)}).attr("stroke",function(e,t){return a(e,t)});var P=D.append("path").each(function(e){this._current=e});M.select("path").transition().attr("d",A).attrTween("d",R);if(l){var H=d3.svg.arc().innerRadius(0);c&&(H=A),h&&(H=d3.svg.arc().outerRadius(A.outerRadius())),_.enter().append("g").classed("nv-label",!0).each(function(e,t){var n=d3.select(this);n.attr("transform",function(e){if(g){e.outerRadius=x+10,e.innerRadius=x+15;var t=(e.startAngle+e.endAngle)/2*(180/Math.PI);return(e.startAngle+e.endAngle)/2v?r[p]:""})}}),S}var t={top:0,right:0,bottom:0,left:0},n=500,r=500,i=function(e){return e.x},s=function(e){return e.y},o=function(e){return e.description},u=Math.floor(Math.random()*1e4),a=e.utils.defaultColor(),f=d3.format(",.2f"),l=!0,c=!0,h=!1,p="key",v=.02,m=!1,g=!1,y=!1,b=!1,w=.5,E=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout");return S.dispatch=E,S.options=e.utils.optionsFunc.bind(S),S.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,S):t},S.width=function(e){return arguments.length?(n=e,S):n},S.height=function(e){return arguments.length?(r=e,S):r},S.values=function(t){return e.log("pie.values() is no longer supported."),S},S.x=function(e){return arguments.length?(i=e,S):i},S.y=function(e){return arguments.length?(s=d3.functor(e),S):s},S.description=function(e){return arguments.length?(o=e,S):o},S.showLabels=function(e){return arguments.length?(l=e,S):l},S.labelSunbeamLayout=function(e){return arguments.length?(g=e,S):g},S.donutLabelsOutside=function(e){return arguments.length?(h=e,S):h},S.pieLabelsOutside=function(e){return arguments.length?(c=e,S):c},S.labelType=function(e){return arguments.length?(p=e,p=p||"key",S):p},S.donut=function(e){return arguments.length?(m=e,S):m},S.donutRatio=function(e){return arguments.length?(w=e,S):w},S.startAngle=function(e){return arguments.length?(y=e,S):y},S.endAngle=function(e){return arguments.length?(b=e,S):b},S.id=function(e){return arguments.length?(u=e,S):u},S.color=function(t){return arguments.length?(a=e.utils.getColor(t),S):a},S.valueFormat=function(e){return arguments.length?(f=e,S):f},S.labelThreshold=function(e){return arguments.length?(v=e,S):v},S},e.models.pieChart=function(){"use strict";function v(e){return e.each(function(e){var u=d3.select(this),a=this,f=(i||parseInt(u.style("width"))||960)-r.left-r.right,d=(s||parseInt(u.style("height"))||400)-r.top-r.bottom;v.update=function(){u.transition().call(v)},v.container=this,l.disabled=e.map(function(e){return!!e.disabled});if(!c){var m;c={};for(m in l)l[m]instanceof Array?c[m]=l[m].slice(0):c[m]=l[m]}if(!e||!e.length){var g=u.selectAll(".nv-noData").data([h]);return g.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),g.attr("x",r.left+f/2).attr("y",r.top+d/2).text(function(e){return e}),v}u.selectAll(".nv-noData").remove();var y=u.selectAll("g.nv-wrap.nv-pieChart").data([e]),b=y.enter().append("g").attr("class","nvd3 nv-wrap nv-pieChart").append("g"),w=y.select("g");b.append("g").attr("class","nv-pieWrap"),b.append("g").attr("class","nv-legendWrap"),o&&(n.width(f).key(t.x()),y.select(".nv-legendWrap").datum(e).call(n),r.top!=n.height()&&(r.top=n.height(),d=(s||parseInt(u.style("height"))||400)-r.top-r.bottom),y.select(".nv-legendWrap").attr("transform","translate(0,"+ -r.top+")")),y.attr("transform","translate("+r.left+","+r.top+")"),t.width(f).height(d);var E=w.select(".nv-pieWrap").datum([e]);d3.transition(E).call(t),n.dispatch.on("stateChange",function(e){l=e,p.stateChange(l),v.update()}),t.dispatch.on("elementMouseout.tooltip",function(e){p.tooltipHide(e)}),p.on("changeState",function(t){typeof t.disabled!="undefined"&&(e.forEach(function(e,n){e.disabled=t.disabled[n]}),l.disabled=t.disabled),v.update()})}),v}var t=e.models.pie(),n=e.models.legend(),r={top:30,right:20,bottom:20,left:20},i=null,s=null,o=!0,u=e.utils.defaultColor(),a=!0,f=function(e,t,n,r){return"

    "+e+"

    "+"

    "+t+"

    "},l={},c=null,h="No Data Available.",p=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),d=function(n,r){var i=t.description()(n.point)||t.x()(n.point),s=n.pos[0]+(r&&r.offsetLeft||0),o=n.pos[1]+(r&&r.offsetTop||0),u=t.valueFormat()(t.y()(n.point)),a=f(i,u,n,v);e.tooltip.show([s,o],a,n.value<0?"n":"s",null,r)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+r.left,e.pos[1]+r.top],p.tooltipShow(e)}),p.on("tooltipShow",function(e){a&&d(e)}),p.on("tooltipHide",function(){a&&e.tooltip.cleanup()}),v.legend=n,v.dispatch=p,v.pie=t,d3.rebind(v,t,"valueFormat","values","x","y","description","id","showLabels","donutLabelsOutside","pieLabelsOutside","labelType","donut","donutRatio","labelThreshold"),v.options=e.utils.optionsFunc.bind(v),v.margin=function(e){return arguments.length?(r.top=typeof e.top!="undefined"?e.top:r.top,r.right=typeof e.right!="undefined"?e.right:r.right,r.bottom=typeof e.bottom!="undefined"?e.bottom:r.bottom,r.left=typeof e.left!="undefined"?e.left:r.left,v):r},v.width=function(e){return arguments.length?(i=e,v):i},v.height=function(e){return arguments.length?(s=e,v):s},v.color=function(r){return arguments.length?(u=e.utils.getColor(r),n.color(u),t.color(u),v):u},v.showLegend=function(e){return arguments.length?(o=e,v):o},v.tooltips=function(e){return arguments.length?(a=e,v):a},v.tooltipContent=function(e){return arguments.length?(f=e,v):f},v.state=function(e){return arguments.length?(l=e,v):l},v.defaultState=function(e){return arguments.length?(c=e,v):c},v.noData=function(e){return arguments.length?(h=e,v):h},v},e.models.scatter=function(){"use strict";function I(q){return q.each(function(I){function Q(){if(!g)return!1;var e,i=d3.merge(I.map(function(e,t){return e.values.map(function(e,n){var r=f(e,n),i=l(e,n);return[o(r)+Math.random()*1e-7,u(i)+Math.random()*1e-7,t,n,e]}).filter(function(e,t){return b(e[4],t)})}));if(D===!0){if(x){var a=X.select("defs").selectAll(".nv-point-clips").data([s]).enter();a.append("clipPath").attr("class","nv-point-clips").attr("id","nv-points-clip-"+s);var c=X.select("#nv-points-clip-"+s).selectAll("circle").data(i);c.enter().append("circle").attr("r",T),c.exit().remove(),c.attr("cx",function(e){return e[0]}).attr("cy",function(e){return e[1]}),X.select(".nv-point-paths").attr("clip-path","url(#nv-points-clip-"+s+")")}i.length&&(i.push([o.range()[0]-20,u.range()[0]-20,null,null]),i.push([o.range()[1]+20,u.range()[1]+20,null,null]),i.push([o.range()[0]-20,u.range()[0]+20,null,null]),i.push([o.range()[1]+20,u.range()[1]-20,null,null]));var h=d3.geom.polygon([[-10,-10],[-10,r+10],[n+10,r+10],[n+10,-10]]),p=d3.geom.voronoi(i).map(function(e,t){return{data:h.clip(e),series:i[t][2],point:i[t][3]}}),d=X.select(".nv-point-paths").selectAll("path").data(p);d.enter().append("path").attr("class",function(e,t){return"nv-path-"+t}),d.exit().remove(),d.attr("d",function(e){return e.data.length===0?"M 0 0":"M"+e.data.join("L")+"Z"});var v=function(e,n){if(F)return 0;var r=I[e.series];if(typeof r=="undefined")return;var i=r.values[e.point];n({point:i,series:r,pos:[o(f(i,e.point))+t.left,u(l(i,e.point))+t.top],seriesIndex:e.series,pointIndex:e.point})};d.on("click",function(e){v(e,_.elementClick)}).on("mouseover",function(e){v(e,_.elementMouseover)}).on("mouseout",function(e,t){v(e,_.elementMouseout)})}else X.select(".nv-groups").selectAll(".nv-group").selectAll(".nv-point").on("click",function(e,n){if(F||!I[e.series])return 0;var r=I[e.series],i=r.values[n];_.elementClick({point:i,series:r,pos:[o(f(i,n))+t.left,u(l(i,n))+t.top],seriesIndex:e.series,pointIndex:n})}).on("mouseover",function(e,n){if(F||!I[e.series])return 0;var r=I[e.series],i=r.values[n];_.elementMouseover({point:i,series:r,pos:[o(f(i,n))+t.left,u(l(i,n))+t.top],seriesIndex:e.series,pointIndex:n})}).on("mouseout",function(e,t){if(F||!I[e.series])return 0;var n=I[e.series],r=n.values[t];_.elementMouseout({point:r,series:n,seriesIndex:e.series,pointIndex:t})});F=!1}var q=n-t.left-t.right,R=r-t.top-t.bottom,U=d3.select(this);I.forEach(function(e,t){e.values.forEach(function(e){e.series=t})});var W=N&&C&&A?[]:d3.merge(I.map(function(e){return e.values.map(function(e,t){return{x:f(e,t),y:l(e,t),size:c(e,t)}})}));o.domain(N||d3.extent(W.map(function(e){return e.x}).concat(d))),w&&I[0]?o.range(k||[(q*E+q)/(2*I[0].values.length),q-q*(1+E)/(2*I[0].values.length)]):o.range(k||[0,q]),u.domain(C||d3.extent(W.map(function(e){return e.y}).concat(v))).range(L||[R,0]),a.domain(A||d3.extent(W.map(function(e){return e.size}).concat(m))).range(O||[16,256]);if(o.domain()[0]===o.domain()[1]||u.domain()[0]===u.domain()[1])M=!0;o.domain()[0]===o.domain()[1]&&(o.domain()[0]?o.domain([o.domain()[0]-o.domain()[0]*.01,o.domain()[1]+o.domain()[1]*.01]):o.domain([-1,1])),u.domain()[0]===u.domain()[1]&&(u.domain()[0]?u.domain([u.domain()[0]-u.domain()[0]*.01,u.domain()[1]+u.domain()[1]*.01]):u.domain([-1,1])),isNaN(o.domain()[0])&&o.domain([-1,1]),isNaN(u.domain()[0])&&u.domain([-1,1]),P=P||o,H=H||u,B=B||a;var X=U.selectAll("g.nv-wrap.nv-scatter").data([I]),V=X.enter().append("g").attr("class","nvd3 nv-wrap nv-scatter nv-chart-"+s+(M?" nv-single-point":"")),$=V.append("defs"),J=V.append("g"),K=X.select("g");J.append("g").attr("class","nv-groups"),J.append("g").attr("class","nv-point-paths"),X.attr("transform","translate("+t.left+","+t.top+")"),$.append("clipPath").attr("id","nv-edge-clip-"+s).append("rect"),X.select("#nv-edge-clip-"+s+" rect").attr("width",q).attr("height",R>0?R:0),K.attr("clip-path",S?"url(#nv-edge-clip-"+s+")":""),F=!0;var G=X.select(".nv-groups").selectAll(".nv-group").data(function(e){return e},function(e){return e.key});G.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),G.exit().remove(),G.attr("class",function(e,t){return"nv-group nv-series-"+t}).classed("hover",function(e){return e.hover}),G.transition().style("fill",function(e,t){return i(e,t)}).style("stroke",function(e,t){return i(e,t)}).style("stroke-opacity",1).style("fill-opacity",.5);if(p){var Y=G.selectAll("circle.nv-point").data(function(e){return e.values},y);Y.enter().append("circle").style("fill",function(e,t){return e.color}).style("stroke",function(e,t){return e.color}).attr("cx",function(t,n){return e.utils.NaNtoZero(P(f(t,n)))}).attr("cy",function(t,n){return e.utils.NaNtoZero(H(l(t,n)))}).attr("r",function(e,t){return Math.sqrt(a(c(e,t))/Math.PI)}),Y.exit().remove(),G.exit().selectAll("path.nv-point").transition().attr("cx",function(t,n){return e.utils.NaNtoZero(o(f(t,n)))}).attr("cy",function(t,n){return e.utils.NaNtoZero(u(l(t,n)))}).remove(),Y.each(function(e,t){d3.select(this).classed("nv-point",!0).classed("nv-point-"+t,!0).classed("hover",!1)}),Y.transition().attr("cx",function(t,n){return e.utils.NaNtoZero(o(f(t,n)))}).attr("cy",function(t,n){return e.utils.NaNtoZero(u(l(t,n)))}).attr("r",function(e,t){return Math.sqrt(a(c(e,t))/Math.PI)})}else{var Y=G.selectAll("path.nv-point").data(function(e){return e.values});Y.enter().append("path").style("fill",function(e,t){return e.color}).style("stroke",function(e,t){return e.color}).attr("transform",function(e,t){return"translate("+P(f(e,t))+","+H(l(e,t))+")"}).attr("d",d3.svg.symbol().type(h).size(function(e,t){return a(c(e,t))})),Y.exit().remove(),G.exit().selectAll("path.nv-point").transition().attr("transform",function(e,t){return"translate("+o(f(e,t))+","+u(l(e,t))+")"}).remove(),Y.each(function(e,t){d3.select(this).classed("nv-point",!0).classed("nv-point-"+t,!0).classed("hover",!1)}),Y.transition().attr("transform",function(e,t){return"translate("+o(f(e,t))+","+u(l(e,t))+")"}).attr("d",d3.svg.symbol().type(h).size(function(e,t){return a(c(e,t))}))}clearTimeout(j),j=setTimeout(Q,300),P=o.copy(),H=u.copy(),B=a.copy()}),I}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=e.utils.defaultColor(),s=Math.floor(Math.random()*1e5),o=d3.scale.linear(),u=d3.scale.linear(),a=d3.scale.linear(),f=function(e){return e.x},l=function(e){return e.y},c=function(e){return e.size||1},h=function(e){return e.shape||"circle"},p=!0,d=[],v=[],m=[],g=!0,y=null,b=function(e){return!e.notActive},w=!1,E=.1,S=!1,x=!0,T=function(){return 25},N=null,C=null,k=null,L=null,A=null,O=null,M=!1,_=d3.dispatch("elementClick","elementMouseover","elementMouseout"),D=!0,P,H,B,j,F=!1;return I.clearHighlights=function(){d3.selectAll(".nv-chart-"+s+" .nv-point.hover").classed("hover",!1)},I.highlightPoint=function(e,t,n){d3.select(".nv-chart-"+s+" .nv-series-"+e+" .nv-point-"+t).classed("hover",n)},_.on("elementMouseover.point",function(e){g&&I.highlightPoint(e.seriesIndex,e.pointIndex,!0)}),_.on("elementMouseout.point",function(e){g&&I.highlightPoint(e.seriesIndex,e.pointIndex,!1)}),I.dispatch=_,I.options=e.utils.optionsFunc.bind(I),I.x=function(e){return arguments.length?(f=d3.functor(e),I):f},I.y=function(e){return arguments.length?(l=d3.functor(e),I):l},I.size=function(e){return arguments.length?(c=d3.functor(e),I):c},I.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,I):t},I.width=function(e){return arguments.length?(n=e,I):n},I.height=function(e){return arguments.length?(r=e,I):r},I.xScale=function(e){return arguments.length?(o=e,I):o},I.yScale=function(e){return arguments.length?(u=e,I):u},I.zScale=function(e){return arguments.length?(a=e,I):a},I.xDomain=function(e){return arguments.length?(N=e,I):N},I.yDomain=function(e){return arguments.length?(C=e,I):C},I.sizeDomain=function(e){return arguments.length?(A=e,I):A},I.xRange=function(e){return arguments.length?(k=e,I):k},I.yRange=function(e){return arguments.length?(L=e,I):L},I.sizeRange=function(e){return arguments.length?(O=e,I):O},I.forceX=function(e){return arguments.length?(d=e,I):d},I.forceY=function(e){return arguments.length?(v=e,I):v},I.forceSize=function(e){return arguments.length?(m=e,I):m},I.interactive=function(e){return arguments.length?(g=e,I):g},I.pointKey=function(e){return arguments.length?(y=e,I):y},I.pointActive=function(e){return arguments.length?(b=e,I):b},I.padData=function(e){return arguments.length?(w=e,I):w},I.padDataOuter=function(e){return arguments.length?(E=e,I):E},I.clipEdge=function(e){return arguments.length?(S=e,I):S},I.clipVoronoi=function(e){return arguments.length?(x=e,I):x},I.useVoronoi=function(e){return arguments.length?(D=e,D===!1&&(x=!1),I):D},I.clipRadius=function(e){return arguments.length?(T=e,I):T},I.color=function(t){return arguments.length?(i=e.utils.getColor(t),I):i},I.shape=function(e){return arguments.length?(h=e,I):h},I.onlyCircles=function(e){return arguments.length?(p=e,I):p},I.id=function(e){return arguments.length?(s=e,I):s},I.singlePoint=function(e){return arguments.length?(M=e,I):M},I},e.models.scatterChart=function(){"use strict";function F(e){return e.each(function(e){function K(){if(T)return X.select(".nv-point-paths").style("pointer-events","all"),!1;X.select(".nv-point-paths").style("pointer-events","none");var i=d3.mouse(this);h.distortion(x).focus(i[0]),p.distortion(x).focus(i[1]),X.select(".nv-scatterWrap").call(t),b&&X.select(".nv-x.nv-axis").call(n),w&&X.select(".nv-y.nv-axis").call(r),X.select(".nv-distributionX").datum(e.filter(function(e){return!e.disabled})).call(o),X.select(".nv-distributionY").datum(e.filter(function(e){return!e.disabled})).call(u)}var C=d3.select(this),k=this,L=(f||parseInt(C.style("width"))||960)-a.left-a.right,I=(l||parseInt(C.style("height"))||400)-a.top-a.bottom;F.update=function(){C.transition().duration(D).call(F)},F.container=this,A.disabled=e.map(function(e){return!!e.disabled});if(!O){var q;O={};for(q in A)A[q]instanceof Array?O[q]=A[q].slice(0):O[q]=A[q]}if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var R=C.selectAll(".nv-noData").data([_]);return R.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),R.attr("x",a.left+L/2).attr("y",a.top+I/2).text(function(e){return e}),F}C.selectAll(".nv-noData").remove(),P=P||h,H=H||p;var U=C.selectAll("g.nv-wrap.nv-scatterChart").data([e]),z=U.enter().append("g").attr("class","nvd3 nv-wrap nv-scatterChart nv-chart-"+t.id()),W=z.append("g"),X=U.select("g");W.append("rect").attr("class","nvd3 nv-background"),W.append("g").attr("class","nv-x nv-axis"),W.append("g").attr("class","nv-y nv-axis"),W.append("g").attr("class","nv-scatterWrap"),W.append("g").attr("class","nv-distWrap"),W.append("g").attr("class","nv-legendWrap"),W.append("g").attr("class","nv-controlsWrap");if(y){var V=S?L/2:L;i.width(V),U.select(".nv-legendWrap").datum(e).call(i),a.top!=i.height()&&(a.top=i.height(),I=(l||parseInt(C.style("height"))||400)-a.top-a.bottom),U.select(".nv-legendWrap").attr("transform","translate("+(L-V)+","+ -a.top+")")}S&&(s.width(180).color(["#444"]),X.select(".nv-controlsWrap").datum(j).attr("transform","translate(0,"+ -a.top+")").call(s)),U.attr("transform","translate("+a.left+","+a.top+")"),E&&X.select(".nv-y.nv-axis").attr("transform","translate("+L+",0)"),t.width(L).height(I).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),d!==0&&t.xDomain(null),v!==0&&t.yDomain(null),U.select(".nv-scatterWrap").datum(e.filter(function(e){return!e.disabled})).call(t);if(d!==0){var $=h.domain()[1]-h.domain()[0];t.xDomain([h.domain()[0]-d*$,h.domain()[1]+d*$])}if(v!==0){var J=p.domain()[1]-p.domain()[0];t.yDomain([p.domain()[0]-v*J,p.domain()[1]+v*J])}(v!==0||d!==0)&&U.select(".nv-scatterWrap").datum(e.filter(function(e){return!e.disabled})).call(t),b&&(n.scale(h).ticks(n.ticks()&&n.ticks().length?n.ticks():L/100).tickSize(-I,0),X.select(".nv-x.nv-axis").attr("transform","translate(0,"+p.range()[0]+")").call(n)),w&&(r.scale(p).ticks(r.ticks()&&r.ticks().length?r.ticks():I/36).tickSize(-L,0),X.select(".nv-y.nv-axis").call(r)),m&&(o.getData(t.x()).scale(h).width(L).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),W.select(".nv-distWrap").append("g").attr("class","nv-distributionX"),X.select(".nv-distributionX").attr("transform","translate(0,"+p.range()[0]+")").datum(e.filter(function(e){return!e.disabled})).call(o)),g&&(u.getData(t.y()).scale(p).width(I).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),W.select(".nv-distWrap").append("g").attr("class","nv-distributionY"),X.select(".nv-distributionY").attr("transform","translate("+(E?L:-u.size())+",0)").datum(e.filter(function(e){return!e.disabled})).call(u)),d3.fisheye&&(X.select(".nv-background").attr("width",L).attr("height",I),X.select(".nv-background").on("mousemove",K),X.select(".nv-background").on("click",function(){T=!T}),t.dispatch.on("elementClick.freezeFisheye",function(){T=!T})),s.dispatch.on("legendClick",function(e,i){e.disabled=!e.disabled,x=e.disabled?0:2.5,X.select(".nv-background").style("pointer-events",e.disabled?"none":"all"),X.select(".nv-point-paths").style("pointer-events",e.disabled?"all":"none"),e.disabled?(h.distortion(x).focus(0),p.distortion(x).focus(0),X.select(".nv-scatterWrap").call(t),X.select(".nv-x.nv-axis").call(n),X.select(".nv-y.nv-axis").call(r)):T=!1,F.update()}),i.dispatch.on("stateChange",function(e){A.disabled=e.disabled,M.stateChange(A),F.update()}),t.dispatch.on("elementMouseover.tooltip",function(e){d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-distx-"+e.pointIndex).attr("y1",function(t,n){return e.pos[1]-I}),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-disty-"+e.pointIndex).attr("x2",e.pos[0]+o.size()),e.pos=[e.pos[0]+a.left,e.pos[1]+a.top],M.tooltipShow(e)}),M.on("tooltipShow",function(e){N&&B(e,k.parentNode)}),M.on("changeState",function(t){typeof t.disabled!="undefined"&&(e.forEach(function(e,n){e.disabled=t.disabled[n]}),A.disabled=t.disabled),F.update()}),P=h.copy(),H=p.copy()}),F}var t=e.models.scatter(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.models.legend(),o=e.models.distribution(),u=e.models.distribution(),a={top:30,right:20,bottom:50,left:75},f=null,l=null,c=e.utils.defaultColor(),h=d3.fisheye?d3.fisheye.scale(d3.scale.linear).distortion(0):t.xScale(),p=d3.fisheye?d3.fisheye.scale(d3.scale.linear).distortion(0):t.yScale(),d=0,v=0,m=!1,g=!1,y=!0,b=!0,w=!0,E=!1,S=!!d3.fisheye,x=0,T=!1,N=!0,C=function(e,t,n){return""+t+""},k=function(e,t,n){return""+n+""},L=null,A={},O=null,M=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),_="No Data Available.",D=250;t.xScale(h).yScale(p),n.orient("bottom").tickPadding(10),r.orient(E?"right":"left").tickPadding(10),o.axis("x"),u.axis("y"),s.updateState(!1);var P,H,B=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),f=i.pos[0]+(s.offsetLeft||0),l=p.range()[0]+a.top+(s.offsetTop||0),c=h.range()[0]+a.left+(s.offsetLeft||0),d=i.pos[1]+(s.offsetTop||0),v=n.tickFormat()(t.x()(i.point,i.pointIndex)),m=r.tickFormat()(t.y()(i.point,i.pointIndex));C!=null&&e.tooltip.show([f,l],C(i.series.key,v,m,i,F),"n",1,s,"x-nvtooltip"),k!=null&&e.tooltip.show([c,d],k(i.series.key,v,m,i,F),"e",1,s,"y-nvtooltip"),L!=null&&e.tooltip.show([o,u],L(i.series.key,v,m,i,F),i.value<0?"n":"s",null,s)},j=[{key:"Magnify",disabled:!0}];return t.dispatch.on("elementMouseout.tooltip",function(e){M.tooltipHide(e),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-distx-"+e.pointIndex).attr("y1",0),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-disty-"+e.pointIndex).attr("x2",u.size())}),M.on("tooltipHide",function(){N&&e.tooltip.cleanup()}),F.dispatch=M,F.scatter=t,F.legend=i,F.controls=s,F.xAxis=n,F.yAxis=r,F.distX=o,F.distY=u,d3.rebind(F,t,"id","interactive","pointActive","x","y","shape","size","xScale","yScale","zScale","xDomain","yDomain","xRange","yRange","sizeDomain","sizeRange","forceX","forceY","forceSize","clipVoronoi","clipRadius","useVoronoi"),F.options=e.utils.optionsFunc.bind(F),F.margin=function(e){return arguments.length?(a.top=typeof e.top!="undefined"?e.top:a.top,a.right=typeof e.right!="undefined"?e.right:a.right,a.bottom=typeof e.bottom!="undefined"?e.bottom:a.bottom,a.left=typeof e.left!="undefined"?e.left:a.left,F):a},F.width=function(e){return arguments.length?(f=e,F):f},F.height=function(e){return arguments.length?(l=e,F):l},F.color=function(t){return arguments.length?(c=e.utils.getColor(t),i.color(c),o.color(c),u.color(c),F):c},F.showDistX=function(e){return arguments.length?(m=e,F):m},F.showDistY=function(e){return arguments.length?(g=e,F):g},F.showControls=function(e){return arguments.length?(S=e,F):S},F.showLegend=function(e){return arguments.length?(y=e,F):y},F.showXAxis=function(e){return arguments.length?(b=e,F):b},F.showYAxis=function(e){return arguments.length?(w=e,F):w},F.rightAlignYAxis=function(e){return arguments.length?(E=e,r.orient(e?"right":"left"),F):E},F.fisheye=function(e){return arguments.length?(x=e,F):x},F.xPadding=function(e){return arguments.length?(d=e,F):d},F.yPadding=function(e){return arguments.length?(v=e,F):v},F.tooltips=function(e){return arguments.length?(N=e,F):N},F.tooltipContent=function(e){return arguments.length?(L=e,F):L},F.tooltipXContent=function(e){return arguments.length?(C=e,F):C},F.tooltipYContent=function(e){return arguments.length?(k=e,F):k},F.state=function(e){return arguments.length?(A=e,F):A},F.defaultState=function(e){return arguments.length?(O=e,F):O},F.noData=function(e){return arguments.length?(_=e,F):_},F.transitionDuration=function(e){return arguments.length?(D=e,F):D},F},e.models.scatterPlusLineChart=function(){"use strict";function B(e){return e.each(function(e){function $(){if(S)return z.select(".nv-point-paths").style("pointer-events","all"),!1;z.select(".nv-point-paths").style("pointer-events","none");var i=d3.mouse(this);h.distortion(E).focus(i[0]),p.distortion(E).focus(i[1]),z.select(".nv-scatterWrap").datum(e.filter(function(e){return!e.disabled})).call(t),g&&z.select(".nv-x.nv-axis").call(n),y&&z.select(".nv-y.nv-axis").call(r),z.select(".nv-distributionX").datum(e.filter(function(e){return!e.disabled})).call(o),z.select(".nv-distributionY").datum(e.filter(function(e){return!e.disabled})).call(u)}var T=d3.select(this),N=this,C=(f||parseInt(T.style("width"))||960)-a.left-a.right,j=(l||parseInt(T.style("height"))||400)-a.top-a.bottom;B.update=function(){T.transition().duration(M).call(B)},B.container=this,k.disabled=e.map(function(e){return!!e.disabled});if(!L){var F;L={};for(F in k)k[F]instanceof Array?L[F]=k[F].slice(0):L[F]=k[F]}if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var I=T.selectAll(".nv-noData").data([O]);return I.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),I.attr("x",a.left+C/2).attr("y",a.top+j/2).text(function(e){return e}),B}T.selectAll(".nv-noData").remove(),h=t.xScale(),p=t.yScale(),_=_||h,D=D||p;var q=T.selectAll("g.nv-wrap.nv-scatterChart").data([e]),R=q.enter().append("g").attr("class","nvd3 nv-wrap nv-scatterChart nv-chart-"+t.id()),U=R.append("g"),z=q.select("g");U.append("rect").attr("class","nvd3 nv-background").style("pointer-events","none"),U.append("g").attr("class","nv-x nv-axis"),U.append("g").attr("class","nv-y nv-axis"),U.append("g").attr("class","nv-scatterWrap"),U.append("g").attr("class","nv-regressionLinesWrap"),U.append("g").attr("class","nv-distWrap"),U.append("g").attr("class","nv-legendWrap"),U.append("g").attr("class","nv-controlsWrap"),q.attr("transform","translate("+a.left+","+a.top+")"),b&&z.select(".nv-y.nv-axis").attr("transform","translate("+C+",0)"),m&&(i.width(C/2),q.select(".nv-legendWrap").datum(e).call(i),a.top!=i.height()&&(a.top=i.height(),j=(l||parseInt(T.style("height"))||400)-a.top-a.bottom),q.select(".nv-legendWrap").attr("transform","translate("+C/2+","+ -a.top+")")),w&&(s.width(180).color(["#444"]),z.select(".nv-controlsWrap").datum(H).attr("transform","translate(0,"+ -a.top+")").call(s)),t.width(C).height(j).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),q.select(".nv-scatterWrap").datum(e.filter(function(e){return!e.disabled})).call(t),q.select(".nv-regressionLinesWrap").attr("clip-path","url(#nv-edge-clip-"+t.id()+")");var W=q.select(".nv-regressionLinesWrap").selectAll(".nv-regLines").data(function(e){return e});W.enter().append("g").attr("class","nv-regLines");var X=W.selectAll(".nv-regLine").data(function(e){return[e]}),V=X.enter().append("line").attr("class","nv-regLine").style("stroke-opacity",0);X.transition().attr("x1",h.range()[0]).attr("x2",h.range()[1]).attr("y1",function(e,t){return p(h.domain()[0]*e.slope+e.intercept)}).attr("y2",function(e,t){return p(h.domain()[1]*e.slope+e.intercept)}).style("stroke",function(e,t,n){return c(e,n)}).style("stroke-opacity",function(e,t){return e.disabled||typeof e.slope=="undefined"||typeof e.intercept=="undefined"?0:1}),g&&(n.scale(h).ticks(n.ticks()?n.ticks():C/100).tickSize(-j,0),z.select(".nv-x.nv-axis").attr("transform","translate(0,"+p.range()[0]+")").call(n)),y&&(r.scale(p).ticks(r.ticks()?r.ticks():j/36).tickSize(-C,0),z.select(".nv-y.nv-axis").call(r)),d&&(o.getData(t.x()).scale(h).width(C).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),U.select(".nv-distWrap").append("g").attr("class","nv-distributionX"),z.select(".nv-distributionX").attr("transform","translate(0,"+p.range()[0]+")").datum(e.filter(function(e){return!e.disabled})).call(o)),v&&(u.getData(t.y()).scale(p).width( +j).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),U.select(".nv-distWrap").append("g").attr("class","nv-distributionY"),z.select(".nv-distributionY").attr("transform","translate("+(b?C:-u.size())+",0)").datum(e.filter(function(e){return!e.disabled})).call(u)),d3.fisheye&&(z.select(".nv-background").attr("width",C).attr("height",j),z.select(".nv-background").on("mousemove",$),z.select(".nv-background").on("click",function(){S=!S}),t.dispatch.on("elementClick.freezeFisheye",function(){S=!S})),s.dispatch.on("legendClick",function(e,i){e.disabled=!e.disabled,E=e.disabled?0:2.5,z.select(".nv-background").style("pointer-events",e.disabled?"none":"all"),z.select(".nv-point-paths").style("pointer-events",e.disabled?"all":"none"),e.disabled?(h.distortion(E).focus(0),p.distortion(E).focus(0),z.select(".nv-scatterWrap").call(t),z.select(".nv-x.nv-axis").call(n),z.select(".nv-y.nv-axis").call(r)):S=!1,B.update()}),i.dispatch.on("stateChange",function(e){k=e,A.stateChange(k),B.update()}),t.dispatch.on("elementMouseover.tooltip",function(e){d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-distx-"+e.pointIndex).attr("y1",e.pos[1]-j),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-disty-"+e.pointIndex).attr("x2",e.pos[0]+o.size()),e.pos=[e.pos[0]+a.left,e.pos[1]+a.top],A.tooltipShow(e)}),A.on("tooltipShow",function(e){x&&P(e,N.parentNode)}),A.on("changeState",function(t){typeof t.disabled!="undefined"&&(e.forEach(function(e,n){e.disabled=t.disabled[n]}),k.disabled=t.disabled),B.update()}),_=h.copy(),D=p.copy()}),B}var t=e.models.scatter(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.models.legend(),o=e.models.distribution(),u=e.models.distribution(),a={top:30,right:20,bottom:50,left:75},f=null,l=null,c=e.utils.defaultColor(),h=d3.fisheye?d3.fisheye.scale(d3.scale.linear).distortion(0):t.xScale(),p=d3.fisheye?d3.fisheye.scale(d3.scale.linear).distortion(0):t.yScale(),d=!1,v=!1,m=!0,g=!0,y=!0,b=!1,w=!!d3.fisheye,E=0,S=!1,x=!0,T=function(e,t,n){return""+t+""},N=function(e,t,n){return""+n+""},C=function(e,t,n,r){return"

    "+e+"

    "+"

    "+r+"

    "},k={},L=null,A=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),O="No Data Available.",M=250;t.xScale(h).yScale(p),n.orient("bottom").tickPadding(10),r.orient(b?"right":"left").tickPadding(10),o.axis("x"),u.axis("y"),s.updateState(!1);var _,D,P=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),f=i.pos[0]+(s.offsetLeft||0),l=p.range()[0]+a.top+(s.offsetTop||0),c=h.range()[0]+a.left+(s.offsetLeft||0),d=i.pos[1]+(s.offsetTop||0),v=n.tickFormat()(t.x()(i.point,i.pointIndex)),m=r.tickFormat()(t.y()(i.point,i.pointIndex));T!=null&&e.tooltip.show([f,l],T(i.series.key,v,m,i,B),"n",1,s,"x-nvtooltip"),N!=null&&e.tooltip.show([c,d],N(i.series.key,v,m,i,B),"e",1,s,"y-nvtooltip"),C!=null&&e.tooltip.show([o,u],C(i.series.key,v,m,i.point.tooltip,i,B),i.value<0?"n":"s",null,s)},H=[{key:"Magnify",disabled:!0}];return t.dispatch.on("elementMouseout.tooltip",function(e){A.tooltipHide(e),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-distx-"+e.pointIndex).attr("y1",0),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-disty-"+e.pointIndex).attr("x2",u.size())}),A.on("tooltipHide",function(){x&&e.tooltip.cleanup()}),B.dispatch=A,B.scatter=t,B.legend=i,B.controls=s,B.xAxis=n,B.yAxis=r,B.distX=o,B.distY=u,d3.rebind(B,t,"id","interactive","pointActive","x","y","shape","size","xScale","yScale","zScale","xDomain","yDomain","xRange","yRange","sizeDomain","sizeRange","forceX","forceY","forceSize","clipVoronoi","clipRadius","useVoronoi"),B.options=e.utils.optionsFunc.bind(B),B.margin=function(e){return arguments.length?(a.top=typeof e.top!="undefined"?e.top:a.top,a.right=typeof e.right!="undefined"?e.right:a.right,a.bottom=typeof e.bottom!="undefined"?e.bottom:a.bottom,a.left=typeof e.left!="undefined"?e.left:a.left,B):a},B.width=function(e){return arguments.length?(f=e,B):f},B.height=function(e){return arguments.length?(l=e,B):l},B.color=function(t){return arguments.length?(c=e.utils.getColor(t),i.color(c),o.color(c),u.color(c),B):c},B.showDistX=function(e){return arguments.length?(d=e,B):d},B.showDistY=function(e){return arguments.length?(v=e,B):v},B.showControls=function(e){return arguments.length?(w=e,B):w},B.showLegend=function(e){return arguments.length?(m=e,B):m},B.showXAxis=function(e){return arguments.length?(g=e,B):g},B.showYAxis=function(e){return arguments.length?(y=e,B):y},B.rightAlignYAxis=function(e){return arguments.length?(b=e,r.orient(e?"right":"left"),B):b},B.fisheye=function(e){return arguments.length?(E=e,B):E},B.tooltips=function(e){return arguments.length?(x=e,B):x},B.tooltipContent=function(e){return arguments.length?(C=e,B):C},B.tooltipXContent=function(e){return arguments.length?(T=e,B):T},B.tooltipYContent=function(e){return arguments.length?(N=e,B):N},B.state=function(e){return arguments.length?(k=e,B):k},B.defaultState=function(e){return arguments.length?(L=e,B):L},B.noData=function(e){return arguments.length?(O=e,B):O},B.transitionDuration=function(e){return arguments.length?(M=e,B):M},B},e.models.sparkline=function(){"use strict";function d(e){return e.each(function(e){var i=n-t.left-t.right,d=r-t.top-t.bottom,v=d3.select(this);s.domain(l||d3.extent(e,u)).range(h||[0,i]),o.domain(c||d3.extent(e,a)).range(p||[d,0]);var m=v.selectAll("g.nv-wrap.nv-sparkline").data([e]),g=m.enter().append("g").attr("class","nvd3 nv-wrap nv-sparkline"),b=g.append("g"),w=m.select("g");m.attr("transform","translate("+t.left+","+t.top+")");var E=m.selectAll("path").data(function(e){return[e]});E.enter().append("path"),E.exit().remove(),E.style("stroke",function(e,t){return e.color||f(e,t)}).attr("d",d3.svg.line().x(function(e,t){return s(u(e,t))}).y(function(e,t){return o(a(e,t))}));var S=m.selectAll("circle.nv-point").data(function(e){function n(t){if(t!=-1){var n=e[t];return n.pointIndex=t,n}return null}var t=e.map(function(e,t){return a(e,t)}),r=n(t.lastIndexOf(o.domain()[1])),i=n(t.indexOf(o.domain()[0])),s=n(t.length-1);return[i,r,s].filter(function(e){return e!=null})});S.enter().append("circle"),S.exit().remove(),S.attr("cx",function(e,t){return s(u(e,e.pointIndex))}).attr("cy",function(e,t){return o(a(e,e.pointIndex))}).attr("r",2).attr("class",function(e,t){return u(e,e.pointIndex)==s.domain()[1]?"nv-point nv-currentValue":a(e,e.pointIndex)==o.domain()[0]?"nv-point nv-minValue":"nv-point nv-maxValue"})}),d}var t={top:2,right:0,bottom:2,left:0},n=400,r=32,i=!0,s=d3.scale.linear(),o=d3.scale.linear(),u=function(e){return e.x},a=function(e){return e.y},f=e.utils.getColor(["#000"]),l,c,h,p;return d.options=e.utils.optionsFunc.bind(d),d.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,d):t},d.width=function(e){return arguments.length?(n=e,d):n},d.height=function(e){return arguments.length?(r=e,d):r},d.x=function(e){return arguments.length?(u=d3.functor(e),d):u},d.y=function(e){return arguments.length?(a=d3.functor(e),d):a},d.xScale=function(e){return arguments.length?(s=e,d):s},d.yScale=function(e){return arguments.length?(o=e,d):o},d.xDomain=function(e){return arguments.length?(l=e,d):l},d.yDomain=function(e){return arguments.length?(c=e,d):c},d.xRange=function(e){return arguments.length?(h=e,d):h},d.yRange=function(e){return arguments.length?(p=e,d):p},d.animate=function(e){return arguments.length?(i=e,d):i},d.color=function(t){return arguments.length?(f=e.utils.getColor(t),d):f},d},e.models.sparklinePlus=function(){"use strict";function v(e){return e.each(function(c){function O(){if(a)return;var e=C.selectAll(".nv-hoverValue").data(u),r=e.enter().append("g").attr("class","nv-hoverValue").style("stroke-opacity",0).style("fill-opacity",0);e.exit().transition().duration(250).style("stroke-opacity",0).style("fill-opacity",0).remove(),e.attr("transform",function(e){return"translate("+s(t.x()(c[e],e))+",0)"}).transition().duration(250).style("stroke-opacity",1).style("fill-opacity",1);if(!u.length)return;r.append("line").attr("x1",0).attr("y1",-n.top).attr("x2",0).attr("y2",b),r.append("text").attr("class","nv-xValue").attr("x",-6).attr("y",-n.top).attr("text-anchor","end").attr("dy",".9em"),C.select(".nv-hoverValue .nv-xValue").text(f(t.x()(c[u[0]],u[0]))),r.append("text").attr("class","nv-yValue").attr("x",6).attr("y",-n.top).attr("text-anchor","start").attr("dy",".9em"),C.select(".nv-hoverValue .nv-yValue").text(l(t.y()(c[u[0]],u[0])))}function M(){function r(e,n){var r=Math.abs(t.x()(e[0],0)-n),i=0;for(var s=0;s2){var h=M.yScale().invert(i.mouseY),p=Infinity,d=null;c.forEach(function(e,t){h=Math.abs(h);var n=Math.abs(e.stackedValue.y0),r=Math.abs(e.stackedValue.y);if(h>=n&&h<=r+n){d=t;return}}),d!=null&&(c[d].highlight=!0)}var v=n.tickFormat()(M.x()(s,a)),m=t.style()=="expand"?function(e,t){return d3.format(".1%")(e)}:function(e,t){return r.tickFormat()(e)};o.tooltip.position({left:f+u.left,top:i.mouseY+u.top}).chartContainer(D.parentNode).enabled(g).valueFormatter(m).data({value:v,series:c})(),o.renderGuideLine(f)}),o.dispatch.on("elementMouseout",function(e){N.tooltipHide(),t.clearHighlights()}),N.on("tooltipShow",function(e){g&&O(e,D.parentNode)}),N.on("changeState",function(e){typeof e.disabled!="undefined"&&y.length===e.disabled.length&&(y.forEach(function(t,n){t.disabled=e.disabled[n]}),S.disabled=e.disabled),typeof e.style!="undefined"&&t.style(e.style),M.update()})}),M}var t=e.models.stackedArea(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.models.legend(),o=e.interactiveGuideline(),u={top:30,right:25,bottom:50,left:60},a=null,f=null,l=e.utils.defaultColor(),c=!0,h=!0,p=!0,d=!0,v=!1,m=!1,g=!0,y=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" on "+t+"

    "},b,w,E=d3.format(",.2f"),S={style:t.style()},x=null,T="No Data Available.",N=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),C=250,k=["Stacked","Stream","Expanded"],L={},A=250;n.orient("bottom").tickPadding(7),r.orient(v?"right":"left"),s.updateState(!1);var O=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=y(i.series.key,a,f,i,M);e.tooltip.show([o,u],l,i.value<0?"n":"s",null,s)};return t.dispatch.on("tooltipShow",function(e){e.pos=[e.pos[0]+u.left,e.pos[1]+u.top],N.tooltipShow(e)}),t.dispatch.on("tooltipHide",function(e){N.tooltipHide(e)}),N.on("tooltipHide",function(){g&&e.tooltip.cleanup()}),M.dispatch=N,M.stacked=t,M.legend=i,M.controls=s,M.xAxis=n,M.yAxis=r,M.interactiveLayer=o,d3.rebind(M,t,"x","y","size","xScale","yScale","xDomain","yDomain","xRange","yRange","sizeDomain","interactive","useVoronoi","offset","order","style","clipEdge","forceX","forceY","forceSize","interpolate"),M.options=e.utils.optionsFunc.bind(M),M.margin=function(e){return arguments.length?(u.top=typeof e.top!="undefined"?e.top:u.top,u.right=typeof e.right!="undefined"?e.right:u.right,u.bottom=typeof e.bottom!="undefined"?e.bottom:u.bottom,u.left=typeof e.left!="undefined"?e.left:u.left,M):u},M.width=function(e){return arguments.length?(a=e,M):a},M.height=function(e){return arguments.length?(f=e,M):f},M.color=function(n){return arguments.length?(l=e.utils.getColor(n),i.color(l),t.color(l),M):l},M.showControls=function(e){return arguments.length?(c=e,M):c},M.showLegend=function(e){return arguments.length?(h=e,M):h},M.showXAxis=function(e){return arguments.length?(p=e,M):p},M.showYAxis=function(e){return arguments.length?(d=e,M):d},M.rightAlignYAxis=function(e){return arguments.length?(v=e,r.orient(e?"right":"left"),M):v},M.useInteractiveGuideline=function(e){return arguments.length?(m=e,e===!0&&(M.interactive(!1),M.useVoronoi(!1)),M):m},M.tooltip=function(e){return arguments.length?(y=e,M):y},M.tooltips=function(e){return arguments.length?(g=e,M):g},M.tooltipContent=function(e){return arguments.length?(y=e,M):y},M.state=function(e){return arguments.length?(S=e,M):S},M.defaultState=function(e){return arguments.length?(x=e,M):x},M.noData=function(e){return arguments.length?(T=e,M):T},M.transitionDuration=function(e){return arguments.length?(A=e,M):A},M.controlsData=function(e){return arguments.length?(k=e,M):k},M.controlLabels=function(e){return arguments.length?typeof e!="object"?L:(L=e,M):L},r.setTickFormat=r.tickFormat,r.tickFormat=function(e){return arguments.length?(E=e,r):E},M}})(); \ No newline at end of file diff --git a/website/static/stats.js b/website/static/stats.js new file mode 100644 index 0000000..8a35b7c --- /dev/null +++ b/website/static/stats.js @@ -0,0 +1,143 @@ +var poolHashrateData; +var poolHashrateChart; + +var statData; +var poolKeys; + +function buildChartData(){ + var pools = {}; + + poolKeys = []; + for (var i = 0; i < statData.length; i++){ + for (var pool in statData[i].pools){ + if (poolKeys.indexOf(pool) === -1) + poolKeys.push(pool); + } + } + + for (var i = 0; i < statData.length; i++) { + var time = statData[i].time * 1000; + for (var f = 0; f < poolKeys.length; f++){ + var pName = poolKeys[f]; + var a = pools[pName] = (pools[pName] || { + hashrate: [] + }); + if (pName in statData[i].pools){ + a.hashrate.push([time, statData[i].pools[pName].hashrate]); + } + else{ + a.hashrate.push([time, 0]); + } + } + } + + poolHashrateData = []; + for (var pool in pools){ + poolHashrateData.push({ + key: pool, + values: pools[pool].hashrate + }); + $('#statsHashrateAvg' + pool).text(getReadableHashRateString(calculateAverageHashrate(pool))); + } +} + +function calculateAverageHashrate(pool) { + var count = 0; + var total = 1; + var avg = 0; + for (var i = 0; i < poolHashrateData.length; i++) { + count = 0; + for (var ii = 0; ii < poolHashrateData[i].values.length; ii++) { + if (pool == null || poolHashrateData[i].key === pool) { + count++; + avg += parseFloat(poolHashrateData[i].values[ii][1]); + } + } + if (count > total) + total = count; + } + avg = avg / total; + return avg; +} + +function getReadableHashRateString(hashrate){ + hashrate = (hashrate * 2); + if (hashrate < 1000000) { + return (Math.round(hashrate / 1000) / 1000 ).toFixed(2)+' Sol/s'; + } + var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ]; + var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1); + hashrate = (hashrate/1000) / Math.pow(1000, i + 1); + return hashrate.toFixed(2) + byteUnits[i]; +} + +function timeOfDayFormat(timestamp){ + var dStr = d3.time.format('%I:%M %p')(new Date(timestamp)); + if (dStr.indexOf('0') === 0) dStr = dStr.slice(1); + return dStr; +} + +function displayCharts(){ + nv.addGraph(function() { + poolHashrateChart = nv.models.lineChart() + .margin({left: 80, right: 30}) + .x(function(d){ return d[0] }) + .y(function(d){ return d[1] }) + .useInteractiveGuideline(true); + + poolHashrateChart.xAxis.tickFormat(timeOfDayFormat); + + poolHashrateChart.yAxis.tickFormat(function(d){ + return getReadableHashRateString(d); + }); + + d3.select('#poolHashrate').datum(poolHashrateData).call(poolHashrateChart); + + return poolHashrateChart; + }); +} + +function triggerChartUpdates(){ + poolHashrateChart.update(); +} + +nv.utils.windowResize(triggerChartUpdates); + +$.getJSON('/api/pool_stats', function(data){ + statData = data; + buildChartData(); + displayCharts(); +}); + +statsSource.addEventListener('message', function(e){ + var stats = JSON.parse(e.data); + statData.push(stats); + + var newPoolAdded = (function(){ + for (var p in stats.pools){ + if (poolKeys.indexOf(p) === -1) + return true; + } + return false; + })(); + + if (newPoolAdded || Object.keys(stats.pools).length > poolKeys.length){ + buildChartData(); + displayCharts(); + } + else { + var time = stats.time * 1000; + for (var f = 0; f < poolKeys.length; f++) { + var pool = poolKeys[f]; + for (var i = 0; i < poolHashrateData.length; i++) { + if (poolHashrateData[i].key === pool) { + poolHashrateData[i].values.shift(); + poolHashrateData[i].values.push([time, pool in stats.pools ? stats.pools[pool].hashrate : 0]); + $('#statsHashrateAvg' + pool).text(getReadableHashRateString(calculateAverageHashrate(pool))); + break; + } + } + } + triggerChartUpdates(); + } +}); diff --git a/website/static/style.css b/website/static/style.css new file mode 100644 index 0000000..4b7b03b --- /dev/null +++ b/website/static/style.css @@ -0,0 +1,68 @@ +html, button, input, select, textarea, .pure-g [class *= "pure-u"], .pure-g-r [class *= "pure-u"]{ + font-family: 'Open Sans', sans-serif; +} + +html{ + background: #2d2d2d; + overflow-y: scroll; +} + +body{ + display: flex; + flex-direction: column; + max-width: 1160px; + margin: 0 auto; +} + +header > .home-menu{ + background: inherit !important; + height: 54px; + display: flex; +} + +header > .home-menu > a.pure-menu-heading, header > .home-menu > ul, header > .home-menu > ul > li{ + display: flex !important; + align-items: center; + justify-content: center; + line-height: normal !important; +} + +header > .home-menu > a.pure-menu-heading{ + color: white; + font-size: 1.5em; +} + +header > .home-menu > ul > li > a{ + color: #ced4d9; +} + +header > .home-menu > ul > li > a:hover, header > .home-menu > ul > li > a:focus{ + background: inherit !important; +} + +header > .home-menu > ul > li > a:hover, header > .home-menu > ul > li.pure-menu-selected > a{ + color: white; +} + +main{ + background-color: #ebf4fa; + position: relative; +} + +footer{ + text-align: center; + color: #b3b3b3; + text-decoration: none; + font-size: 0.8em; + padding: 15px; + line-height: 24px; +} + +footer a{ + color: #fff; + text-decoration: none; +} + +footer iframe{ + vertical-align: middle; +} \ No newline at end of file