├── .gitignore ├── LICENSE.md ├── README.md ├── app.js ├── config ├── development.json ├── index.js └── production.json ├── controllers ├── active.js ├── complete.js ├── delayed.js ├── failed.js ├── index.js ├── jobs.js ├── newjob.js ├── pending.js └── queues.js ├── index.js ├── lib ├── enforceConnection.js ├── redisConnector.js ├── server.js ├── setupAndMiddleware.js └── updateInfo.js ├── models ├── bull.js └── redis.js ├── package.json └── public ├── css ├── bootstrap-theme.min.css ├── bootstrap.min.css ├── font-awesome.css └── master.css ├── fonts ├── FontAwesome.otf ├── fontawesome-webfont.eot ├── fontawesome-webfont.svg ├── fontawesome-webfont.ttf └── fontawesome-webfont.woff ├── img └── close.png ├── js ├── .jshintignore ├── .jshintrc ├── ChartHandler.js ├── DataModel.js ├── RedisHandler.js ├── bootstrap.min.js ├── flot │ ├── excanvas.js │ ├── excanvas.min.js │ ├── jquery.colorhelpers.js │ ├── jquery.colorhelpers.min.js │ ├── jquery.flot.canvas.js │ ├── jquery.flot.canvas.min.js │ ├── jquery.flot.categories.js │ ├── jquery.flot.categories.min.js │ ├── jquery.flot.crosshair.js │ ├── jquery.flot.crosshair.min.js │ ├── jquery.flot.errorbars.js │ ├── jquery.flot.errorbars.min.js │ ├── jquery.flot.fillbetween.js │ ├── jquery.flot.fillbetween.min.js │ ├── jquery.flot.image.js │ ├── jquery.flot.image.min.js │ ├── jquery.flot.js │ ├── jquery.flot.min.js │ ├── jquery.flot.navigate.js │ ├── jquery.flot.navigate.min.js │ ├── jquery.flot.pie.js │ ├── jquery.flot.pie.min.js │ ├── jquery.flot.resize.js │ ├── jquery.flot.resize.min.js │ ├── jquery.flot.selection.js │ ├── jquery.flot.selection.min.js │ ├── jquery.flot.stack.js │ ├── jquery.flot.stack.min.js │ ├── jquery.flot.symbol.js │ ├── jquery.flot.symbol.min.js │ ├── jquery.flot.threshold.js │ ├── jquery.flot.threshold.min.js │ ├── jquery.flot.time.js │ └── jquery.flot.time.min.js ├── jquery-2.1.0.min.js ├── jquery.blockUI.js ├── jquery.noty.packaged.min.js ├── knockout.min.js ├── lodash.min.js ├── moment.min.js └── noty │ ├── jquery.noty.js │ ├── layouts │ ├── bottom.js │ ├── bottomCenter.js │ ├── bottomLeft.js │ ├── bottomRight.js │ ├── center.js │ ├── centerLeft.js │ ├── centerRight.js │ ├── inline.js │ ├── top.js │ ├── topCenter.js │ ├── topLeft.js │ └── topRight.js │ ├── packaged │ └── jquery.noty.packaged.js │ ├── promise.js │ └── themes │ └── default.js └── templates ├── errors ├── 404.dust └── not-connected.dust ├── index.dust ├── jobList.dust ├── layouts └── master.dust ├── newJob.dust └── queueList.dust /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | .build/ 4 | *.iml 5 | node_modules/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Shane King 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Matador 2 | 3 | A node-based web interface for the Bull Job Manager 4 | *** 5 | 6 | **Update Note version 0.1.0 -> 1.0.0** 7 | In the latest update, Matador went from relying on Kraken to relying on just express/dust (which is why I incremented by a major version number). This occurred because Kraken has changed quite a bit since Matador was made, with quite a bit of the previous code becoming deprecated. I also re-worked some of the code to try to make it quicker and resolve some of the issues/feature requests. 8 | 9 | **Why?** 10 | We needed a job manager and we wanted to stick to one that only really relied on Node and Redis, so we looked and looked until we found Bull. Bull looked really nice, but we also wanted to be able to monitor the jobs without having to actually log into the AWS server to access the Redis database. Thus, Matador was born. 11 | 12 | **Why not just use Kue?** 13 | Kue has some really old, outstanding bugs that we encounted just while testing it. We tested Bull quite a bit, and couldn't reproduce these bugs. We thought it'd be easier to build an interface for Bull than to use Kue and deal with those bugs. (Notice that the first one, while closed, was never actually fixed...) 14 | 15 | ##Getting Started## 16 | 17 | ###Installing### 18 | 19 | Easy! If you're using Bull already, then all you need to do is clone this repo and run 20 | 21 | `npm install` 22 | 23 | ###Running standalone from npm### 24 | 25 | You can run the app standalone with 26 | 27 | `node index.js` or `npm start` 28 | 29 | This standalone method will require you to modify the config/development.json and config/production.json files so that it has the right values for your host and port for your redis server (also any additional redis options, such as passwords). 30 | 31 | Or you can simply 32 | 33 | `npm install bull-ui` 34 | 35 | ```js 36 | var app = require('bull-ui/app')(options); 37 | app.listen(1337, function(){ 38 | console.log('bull-ui started listening on port', this.address().port); 39 | }); 40 | 41 | // http://localhost:1337/ 42 | ``` 43 | 44 | Where options is completely optional. If not specified, it will default to the development settings in config. 45 | You can also pass in your own redis configuration thusly: 46 | 47 | ```js 48 | var matador = require('bull-ui/app')({ 49 | redis: { 50 | host: your host name, 51 | port: your port number, 52 | password: optional auth password 53 | } 54 | }); 55 | ``` 56 | 57 | Or use a URL: 58 | 59 | ```js 60 | var matador = require('bull-ui/app')({ 61 | redis: { 62 | url: "redis://u:password@hostname", 63 | } 64 | }); 65 | ``` 66 | 67 | If you are including matador inside of another express app, declare the basepath when you mount it to the other app. 68 | 69 | ```js 70 | var app = someExpressApp(); 71 | var matador = require('bull-ui/app')(options); 72 | 73 | app.use('/matador', function(req, res, next){ 74 | req.basepath = '/matador'; 75 | res.locals.basepath = '/matador'; 76 | next(); 77 | }, matador); 78 | 79 | app.listen(9000); 80 | 81 | // http://localhost:9000/matador 82 | ``` 83 | 84 | If you're not using Bull, and you think you want to use Matador for some reason, then you should go check out Bull over here. After that, if you decide you like it, come back and check out Matador! 85 | 86 | 87 | **What is it built on?** 88 | Matador requires Node (obviously) and is built on Experess. Other NPM packages utilized include, but are probably not limited to: 89 | 90 | * Lodash 91 | * Redis 92 | * Q 93 | 94 | On top of that, Matador also utilizes several open-source javascript/css libraries and tools, including but not limited to: 95 | 96 | * jQuery 97 | * Knockout 98 | * blockUI 99 | * noty 100 | * Chart.js 101 | * Bootstrap 102 | * Font Awesome 103 | 104 | 105 | **Screenshot** 106 | 107 | 108 | Overview 109 | 110 | **Q/A:** 111 | 112 | 113 | *What is a stuck job?* 114 | A stuck job is a job that has no state. It's possible that jobs listed as stuck are just in between states, but if there's ever a job with no state for a long time, the overview page gives you a place where you can delete or revert such jobs back the pending queue. Because stuck jobs are any job without a state, and while jobs are processing they may become stateless, there is no way to mass-manage stuck jobs. This is because it would be really easy to accidentally modify jobs you didn't mean to if a job was being processed while you were mass managing "stuck jobs". 115 | 116 | 117 | **Wrap-up** 118 | 119 | 120 | Have an issue? Feel free to open an issue in the issues page. You can also send me a tweet and I'd be glad to respond! 121 | 122 | If you wanna help, feel free to fork and make pull requests! The general layout and feel of Matador is pretty basic, and I tried to keep my code relatively clean (though, some of it is pretty awful looking right now...sorry about that). 123 | 124 | *** 125 | **License** 126 | *** 127 | 128 | The MIT License (MIT) 129 | 130 | Copyright (c) 2014 Shane King 131 | 132 | Permission is hereby granted, free of charge, to any person obtaining a copy 133 | of this software and associated documentation files (the "Software"), to deal 134 | in the Software without restriction, including without limitation the rights 135 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 136 | copies of the Software, and to permit persons to whom the Software is 137 | furnished to do so, subject to the following conditions: 138 | 139 | The above copyright notice and this permission notice shall be included in 140 | all copies or substantial portions of the Software. 141 | 142 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 143 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 144 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 145 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 146 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 147 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 148 | THE SOFTWARE. 149 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = require('./lib/server'); 4 | 5 | /** 6 | * @param {Object} options 7 | * @param {Object} [options.redis] optional redis settings (host, port, password). 8 | * Defaults to the development redis settings in ./config 9 | * 10 | */ 11 | module.exports = function(options) { 12 | require('./lib/setupAndMiddleware')(app, options); 13 | return app; 14 | }; -------------------------------------------------------------------------------- /config/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 1337, 3 | "redis": { 4 | "host": "localhost", 5 | "port": 6379 6 | }, 7 | "errorPages": { 8 | "404": "errors/404", 9 | "not-connected": "errors/not-connected" 10 | }, 11 | "development": true 12 | } 13 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = require('./'+(process.env.NODE_ENV ? process.env.NODE_ENV.toLowerCase() : "development")); -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 1337, 3 | "redis": { 4 | "host": "localhost", 5 | "port": 6379 6 | }, 7 | "errorPages": { 8 | "404": "errors/404", 9 | "not-connected": "errors/not-connected" 10 | }, 11 | "production": true 12 | } 13 | -------------------------------------------------------------------------------- /controllers/active.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var redisModel = require('../models/redis'), 5 | q = require('q'); 6 | 7 | 8 | module.exports = function (app) { 9 | var requestActive = function(req, res){ 10 | var dfd = q.defer(); 11 | redisModel.getStatus("active").done(function(active){ 12 | redisModel.getJobsInList(active).done(function(keys){ 13 | redisModel.formatKeys(keys).done(function(formattedKeys){ 14 | redisModel.getProgressForKeys(formattedKeys).done(function(keyList){ 15 | redisModel.getStatusCounts().done(function(countObject){ 16 | var model = { keys: keyList, counts: countObject, active: true, type: "Active" }; 17 | dfd.resolve(model); 18 | }); 19 | }); 20 | }); 21 | }); 22 | }); 23 | return dfd.promise; 24 | } 25 | 26 | app.get('/active', function (req, res) { 27 | requestActive(req, res).done(function(model){ 28 | res.render('jobList', model); 29 | }); 30 | }); 31 | 32 | app.get('/api/active', function (req, res) { 33 | requestActive(req, res).done(function(model){ 34 | res.json(model); 35 | }); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /controllers/complete.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var redisModel = require('../models/redis'), 5 | q = require('q'); 6 | 7 | 8 | module.exports = function (app) { 9 | var requestComplete = function(req, res){ 10 | var dfd = q.defer(); 11 | redisModel.getStatus("complete").done(function(completed){ 12 | redisModel.getJobsInList(completed).done(function(keys){ 13 | redisModel.formatKeys(keys).done(function(keyList){ 14 | redisModel.getStatusCounts().done(function(countObject){ 15 | var model = { keys: keyList, counts: countObject, complete: true, type: "Complete" }; 16 | dfd.resolve(model); 17 | }); 18 | }); 19 | }); 20 | }); 21 | return dfd.promise; 22 | }; 23 | 24 | app.get('/complete', function (req, res) { 25 | requestComplete(req, res).done(function(model){ 26 | res.render('jobList', model); 27 | }); 28 | }); 29 | 30 | app.get('/api/complete', function (req, res) { 31 | requestComplete(req, res).done(function(model){ 32 | res.json(model); 33 | }); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /controllers/delayed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var redisModel = require('../models/redis'), 5 | moment = require('moment'), 6 | q = require('q'); 7 | 8 | 9 | module.exports = function (app) { 10 | var getDelayedModel = function(req, res){ 11 | var dfd = q.defer(); 12 | redisModel.getStatus("delayed").done(function(delayed){ 13 | redisModel.getJobsInList(delayed).done(function(keys){ 14 | redisModel.formatKeys(keys).done(function(formattedKeys){ 15 | redisModel.getDelayTimeForKeys(formattedKeys).done(function(keyList){ 16 | redisModel.getStatusCounts().done(function(countObject){ 17 | keyList = keyList.map(function (key) { 18 | var numSecondsUntil = moment(new Date(key.delayUntil)).diff(moment(), 'seconds'); 19 | var formattedDelayUntil = 'in ' + numSecondsUntil + ' seconds'; 20 | if (numSecondsUntil === 1) { 21 | formattedDelayUntil = 'in ' + numSecondsUntil + ' second'; 22 | } 23 | else if (numSecondsUntil > 60) { 24 | formattedDelayUntil = moment(new Date(key.delayUntil)).fromNow(); 25 | } 26 | 27 | key.delayUntil = formattedDelayUntil; 28 | return key; 29 | 30 | }); 31 | var model = { keys: keyList, counts: countObject, delayed: true, type: "Delayed" }; 32 | dfd.resolve(model); 33 | }); 34 | }); 35 | }); 36 | }); 37 | }); 38 | return dfd.promise; 39 | }; 40 | 41 | app.get('/delayed', function (req, res) { 42 | getDelayedModel(req, res).done(function(model){ 43 | res.render('jobList', model); 44 | }); 45 | }); 46 | 47 | app.get('/api/delayed', function (req, res) { 48 | getDelayedModel(req, res).done(function(model){ 49 | res.json(model); 50 | }); 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /controllers/failed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var redisModel = require('../models/redis'), 5 | q = require('q'); 6 | 7 | 8 | module.exports = function (app) { 9 | var getFailedData = function(req, res){ 10 | var dfd = q.defer(); 11 | redisModel.getStatus("failed").done(function(failed){ 12 | redisModel.getJobsInList(failed).done(function(keys){ 13 | redisModel.formatKeys(keys).done(function(keyList){ 14 | redisModel.getStatusCounts().done(function(countObject){ 15 | var model = { keys: keyList, counts: countObject, failed: true, type: "Failed"}; 16 | dfd.resolve(model); 17 | }); 18 | }); 19 | }); 20 | }); 21 | return dfd.promise; 22 | } 23 | 24 | app.get('/failed', function (req, res) { 25 | getFailedData(req, res).done(function(model){ 26 | res.render('jobList', model); 27 | }); 28 | }); 29 | 30 | app.get('/api/failed', function (req, res) { 31 | getFailedData(req, res).done(function(model){ 32 | res.json(model); 33 | }); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /controllers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var redisModel = require('../models/redis'), 3 | _ = require('lodash'), 4 | q = require('q'), 5 | updateInfo = require('../lib/updateInfo.js'); 6 | 7 | module.exports = function (app) { 8 | var getOverviewData = function(req, res){ 9 | var dfd = q.defer(); 10 | redisModel.getAllKeys().done(function(keys){ 11 | redisModel.formatKeys(keys).done(function(keyList){ 12 | redisModel.getStatusCounts().done(function(countObject){ 13 | updateInfo.getMemoryUsage().done(function(memoryUsage){ 14 | if(countObject.stuck == 0) keyList = []; 15 | else keyList = _.filter(keyList, function(key){ return key.status === "stuck"; }); 16 | var usage = []; 17 | for(var time in memoryUsage.usage){ 18 | usage.push({time: time, memory: memoryUsage.usage[time]}); 19 | } 20 | memoryUsage.usage = usage; 21 | var model = { keys: keyList, counts: countObject, overview: true, memory: memoryUsage }; 22 | dfd.resolve(model); 23 | }); 24 | }); 25 | }); 26 | }); 27 | return dfd.promise; 28 | } 29 | 30 | app.get('/', function (req, res) { 31 | getOverviewData(req, res).done(function(model){ 32 | res.render('index', model); 33 | }); 34 | }); 35 | 36 | app.get('/api/', function (req, res) { 37 | getOverviewData(req, res).done(function(model){ 38 | res.json(model); 39 | }); 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /controllers/jobs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bullModel = require('../models/bull'); 4 | var redisModel = require('../models/redis'); 5 | 6 | module.exports = function (app) { 7 | app.get('/api/jobs/pending/status/:type', function (req, res) { 8 | var type = req.params['type']; 9 | redisModel.makePendingByType(type).done(function(results){ 10 | res.json(results); 11 | }); 12 | }); 13 | 14 | app.get('/api/jobs/pending/id/:type/:id', function (req, res) { 15 | var id = req.params['id'], 16 | type = req.params['type']; 17 | redisModel.makePendingById(type, id).done(function(results){ 18 | res.json(results); 19 | }); 20 | }); 21 | 22 | app.get('/api/jobs/delete/status/:type', function (req, res) { 23 | var type = req.params['type']; 24 | var queueName = req.params['queueName'] ? req.params['queueName'] : null; 25 | redisModel.deleteJobByStatus(type, queueName).done(function(results){ 26 | res.json(results); 27 | }); 28 | }); 29 | 30 | app.get('/api/jobs/delete/id/:type/:id', function (req, res) { 31 | var id = req.params['id'], 32 | type = req.params['type']; 33 | redisModel.deleteJobById(type, id).done(function(results){ 34 | res.json(results); 35 | }); 36 | }); 37 | 38 | app.get('/api/jobs/info/:type/:id', function(req, res){ 39 | var id = req.params['id'], 40 | type = req.params['type']; 41 | redisModel.getDataById(type, id).done(function(results){ 42 | res.json(results); 43 | }); 44 | }); 45 | 46 | app.post('/api/jobs/create', function(req, res){ 47 | var error; 48 | var payloadObject; 49 | var payload = req.body.payload; 50 | var queue = req.body && req.body.queue; 51 | if (!queue){ 52 | error = 'No queue specified'; 53 | } 54 | if (!error){ 55 | try { 56 | payloadObject = JSON.parse(req.body.payload); 57 | } catch (e) { 58 | error = 'Invalid JSON'; 59 | } 60 | } 61 | if (error) { 62 | return res.status(400).send(error); 63 | } else { 64 | bullModel.createJob(req.app.locals.options.redis, queue, payloadObject) 65 | .done(function(){return res.status(200).send('OK');}); 66 | } 67 | 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /controllers/newjob.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var q = require('q') 4 | 5 | 6 | var redisModel = require('../models/redis'); 7 | 8 | module.exports = function (app) { 9 | 10 | var getNewJobModel = function(req, res){ 11 | var dfd = q.defer(); 12 | redisModel.getStatusCounts().done(function(countObject){ 13 | var model = { counts: countObject, newjob: true, type: "New Job" }; 14 | dfd.resolve(model); 15 | }); 16 | return dfd.promise; 17 | }; 18 | 19 | app.get('/newjob', function (req, res) { 20 | getNewJobModel(req, res).done(function(model){ 21 | res.render('newJob', model); 22 | }); 23 | }); 24 | 25 | app.get('/api/newjob', function (req, res) { 26 | getNewJobModel(req, res).done(function(model){ 27 | res.json(model); 28 | }); 29 | }); 30 | 31 | }; 32 | -------------------------------------------------------------------------------- /controllers/pending.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var redisModel = require('../models/redis'), 5 | q = require('q'); 6 | 7 | 8 | module.exports = function (app) { 9 | var getPendingModel = function(req, res){ 10 | var dfd = q.defer(); 11 | redisModel.getStatus("wait").done(function(active){ 12 | redisModel.getJobsInList(active).done(function(keys){ 13 | redisModel.formatKeys(keys).done(function(keyList){ 14 | redisModel.getStatusCounts().done(function(countObject){ 15 | var model = { keys: keyList, counts: countObject, pending: true, type: "Pending" }; 16 | dfd.resolve(model); 17 | }); 18 | }); 19 | }); 20 | }); 21 | return dfd.promise; 22 | }; 23 | 24 | app.get('/pending', function (req, res) { 25 | getPendingModel(req, res).done(function(model){ 26 | res.render('jobList', model); 27 | }); 28 | }); 29 | 30 | app.get('/api/pending', function (req, res) { 31 | getPendingModel(req, res).done(function(model){ 32 | res.json(model); 33 | }); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /controllers/queues.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var redisModel = require('../models/redis'), 5 | q = require('q'); 6 | 7 | 8 | module.exports = function (app) { 9 | var getQueuesModel = function(req, res){ 10 | var dfd = q.defer(); 11 | redisModel.getQueues().done(function(queues){ 12 | redisModel.getStatusCounts().done(function(countObject){ 13 | var model = { keys: queues, counts: countObject, queues: true, type: "Queues" }; 14 | dfd.resolve(model); 15 | }); 16 | }); 17 | return dfd.promise; 18 | }; 19 | 20 | app.get('/queues', function (req, res) { 21 | getQueuesModel(req, res).done(function(model){ 22 | res.render('queueList', model); 23 | }); 24 | }); 25 | 26 | app.get('/api/queues', function (req, res) { 27 | getQueuesModel(req, res).done(function(model){ 28 | res.json(model); 29 | }); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var config = require('./config'), 2 | app = require('./app')(config); 3 | 4 | app.listen(config.port, function() { 5 | console.log("Matador listening on port", config.port, "in", process.env.NODE_ENV, "mode"); 6 | }); -------------------------------------------------------------------------------- /lib/enforceConnection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = exports = function (options){ 3 | options = options || {}; 4 | return function (req, res, next) { 5 | if (!redis.connected) { 6 | if (req.xhr) { 7 | res.json({success: false, message: "Not connected to redis database."}); 8 | } else { 9 | res.render(options.errorPages["not-connected"]); 10 | } 11 | } else { 12 | next(); 13 | } 14 | }; 15 | }; -------------------------------------------------------------------------------- /lib/redisConnector.js: -------------------------------------------------------------------------------- 1 | var redisAdapter = require('redis'), 2 | Promise = require('bluebird'), 3 | updateInfo = require('./updateInfo'); 4 | 5 | exports.connect = function(settings){ 6 | Promise.promisifyAll(redisAdapter); 7 | if(settings.url) { 8 | redis = redisAdapter.createClient(settings.url, settings.options); 9 | } else { 10 | redis = redisAdapter.createClient(settings.port, settings.host, settings.options); 11 | if(settings.password){ 12 | redis.auth(settings.password); 13 | } 14 | } 15 | redis.on("error", console.log); 16 | updateInfo.startUpdatingInfo(); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | cons = require('consolidate'), 3 | dust = require('dustjs-linkedin'), 4 | app = express(); 5 | 6 | app.engine('dust', cons.dust); 7 | app.set('view engine', 'dust'); 8 | app.set("views", __dirname + "/../public/templates/"); 9 | 10 | module.exports = app; -------------------------------------------------------------------------------- /lib/setupAndMiddleware.js: -------------------------------------------------------------------------------- 1 | var redisAdapter = require('./redisConnector'), 2 | express = require('express'); 3 | var bodyParser = require('body-parser'); 4 | 5 | module.exports = function(app, options){ 6 | options = options || {}; 7 | options.errorPages = options.errorPages || {}; 8 | options.errorPages['not-connected'] = options.errorPages['not-connected'] || 'errors/not-connected'; 9 | options.errorPages['404'] = options.errorPages['404'] || 'errors/404' 10 | 11 | app.locals.options = options; 12 | 13 | if (!options.redis){ 14 | throw new Error('No redis configuration options passed to matador'); 15 | } 16 | //Connect to redis 17 | redisAdapter.connect(options.redis); 18 | var redisConnectionEnforcer = require('./enforceConnection')(options); 19 | 20 | //Enforce that redis is connected to, will make render an error page if not connected 21 | app.use(redisConnectionEnforcer); 22 | 23 | app.use(bodyParser.urlencoded({extended: true})); 24 | 25 | //Publicly accessible routes 26 | app.use('/css/', express.static(__dirname + '/../public/css')); 27 | app.use('/fonts/', express.static(__dirname + '/../public/fonts')); 28 | app.use('/img/', express.static(__dirname + '/../public/img')); 29 | app.use('/js/', express.static(__dirname + '/../public/js')); 30 | 31 | //Setup routes 32 | require('../controllers/index')(app); 33 | require('../controllers/active')(app); 34 | require('../controllers/complete')(app); 35 | require('../controllers/failed')(app); 36 | require('../controllers/jobs')(app); 37 | require('../controllers/pending')(app); 38 | require('../controllers/delayed')(app); 39 | require('../controllers/queues')(app); 40 | require('../controllers/newjob')(app); 41 | 42 | //404 43 | app.get('*', function(req, res){ 44 | res.render(options.errorPages["404"]); 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /lib/updateInfo.js: -------------------------------------------------------------------------------- 1 | var q = require('q'); 2 | 3 | var memoryKey = "matador:memory"; 4 | var peakMemoryUsage = ""; 5 | var peakMemoryUsageHuman = ""; 6 | 7 | var clearOldData = function(){ 8 | //Deletes any memory usage data older than 15 minutes (or, if you changed the update interval, (updateInterval*30)/60 minutes 9 | var maximumAmountOfKeys = 20; 10 | redis.hgetall(memoryKey, function(err, data){ 11 | var keys = Object.keys(data); 12 | if(keys.length > maximumAmountOfKeys){ 13 | var difference = keys.length - maximumAmountOfKeys; 14 | var multi = []; 15 | for(var i = 0; i < difference; i++){ 16 | multi.push(["hdel", memoryKey, keys[i]]); 17 | } 18 | redis.multi(multi).exec(); 19 | } 20 | }); 21 | } 22 | 23 | var updateInfo = function(){ 24 | var dfd = q.defer(); 25 | redis.info(function(err, data){ 26 | var now = new Date().getTime(); 27 | data = data.split("\r\n"); 28 | var usedMemoryBytes = "", 29 | usedMemoryHuman = ""; 30 | for(var i = 0, ii = data.length; i < ii; i++){ 31 | var infoString = data[i]; 32 | if(infoString.indexOf("used_memory:") === 0){ 33 | usedMemoryBytes = infoString.split(":")[1]; 34 | }else if(infoString.indexOf("used_memory_human:") === 0){ 35 | usedMemoryHuman = infoString.split(":")[1]; 36 | }else if(infoString.indexOf("used_memory_peak:") === 0){ 37 | peakMemoryUsage = infoString.split(":")[1]; 38 | }else if(infoString.indexOf("used_memory_peak_human") === 0){ 39 | peakMemoryUsageHuman = infoString.split(":")[1]; 40 | } 41 | } 42 | redis.hset(memoryKey, now, usedMemoryBytes+":"+usedMemoryHuman, function(err, data){ 43 | clearOldData(); 44 | }); 45 | dfd.resolve(); 46 | }); 47 | return dfd.promise; 48 | }; 49 | 50 | exports.startUpdatingInfo = function(){ 51 | redis.del(memoryKey); //Clears old data when the app starts 52 | var updateInterval = 60; //In seconds 53 | updateInfo().done(function(){ 54 | setInterval(function(){ 55 | updateInfo(); 56 | }, updateInterval*1000) 57 | }); 58 | }; 59 | 60 | exports.getMemoryUsage = function(){ 61 | var dfd = q.defer(); 62 | redis.hgetall(memoryKey, function(err, data){ 63 | dfd.resolve({peak: {bytes: peakMemoryUsage, human: peakMemoryUsageHuman}, usage: data}); 64 | }); 65 | return dfd.promise; 66 | }; -------------------------------------------------------------------------------- /models/bull.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require("lodash"); 4 | var Promise = require('bluebird'); 5 | var q = require('q'); 6 | var Queue = require('bull'); 7 | 8 | var createQueue = _.memoize( 9 | function( queue, port, host, options ){ 10 | return Queue( queue, port, host, options); 11 | }, 12 | function(queue, port, host, options){ 13 | return queue; 14 | } 15 | ); 16 | 17 | var createJob = function createJob(redisOptions, queueName, payload){ 18 | var options = redisOptions.options || {}; 19 | if(redisOptions.password){ 20 | options.auth_pass = redisOptions.password; 21 | } 22 | var queue = createQueue(queueName, redisOptions.port, redisOptions.host, options); 23 | return queue.add(payload); 24 | }; 25 | 26 | module.exports.createJob = createJob; // Creates a job 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bull-ui", 3 | "version": "1.2.3", 4 | "description": "Front-end web interface for Bull Job Manager", 5 | "main": "index.js", 6 | "dependencies": { 7 | "bluebird": "^2.9.24", 8 | "body-parser": "^1.13.3", 9 | "bull": "^1.0.0-rc1", 10 | "consolidate": "~0.10.0", 11 | "dustjs-linkedin": "~2.5.1", 12 | "express": "^4.13.4", 13 | "less": "~2.1.2", 14 | "lodash": "~2.4.1", 15 | "moment": "^2.17.1", 16 | "q": "~1.1.2", 17 | "redis": "^2.5.3" 18 | }, 19 | "devDependencies": {}, 20 | "scripts": { 21 | "test": "echo \"Error: no test specified\" && exit 1", 22 | "start": "node index.js" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git://github.com/ShaneK/Matador.git" 27 | }, 28 | "keywords": [ 29 | "matador", 30 | "bull", 31 | "redis", 32 | "job" 33 | ], 34 | "author": "Shane King <=> (http://lotsofprojects.com/)", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/ShaneK/Matador/issues" 38 | }, 39 | "homepage": "https://github.com/ShaneK/Matador" 40 | } 41 | -------------------------------------------------------------------------------- /public/css/master.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Base structure 3 | */ 4 | 5 | /* Move down content because we have a fixed navbar that is 50px tall */ 6 | /*body {*/ 7 | /*padding-top: 50px;*/ 8 | /*}*/ 9 | 10 | 11 | /* 12 | * Global add-ons 13 | */ 14 | 15 | 16 | body { 17 | padding-top: 50px; 18 | } 19 | 20 | .sub-header { 21 | padding-bottom: 10px; 22 | border-bottom: 1px solid #eee; 23 | } 24 | 25 | 26 | /* 27 | * Navbar 28 | */ 29 | 30 | .navbar { 31 | display: block; 32 | } 33 | 34 | 35 | @media (min-width: 768px) { 36 | .navbar { 37 | display: none; 38 | } 39 | body { 40 | padding-top: 0px; 41 | } 42 | } 43 | 44 | /* 45 | * Sidebar 46 | */ 47 | 48 | /* Hide for mobile, show later */ 49 | .sidebar { 50 | display: none; 51 | } 52 | @media (min-width: 768px) { 53 | .sidebar { 54 | position: fixed; 55 | top: 0px; 56 | bottom: 0; 57 | left: 0; 58 | z-index: 1000; 59 | display: block; 60 | padding: 20px; 61 | overflow-x: hidden; 62 | overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ 63 | background-color: #f5f5f5; 64 | border-right: 1px solid #eee; 65 | } 66 | } 67 | 68 | /* Sidebar navigation */ 69 | .nav-sidebar { 70 | margin-right: -21px; /* 20px padding + 1px border */ 71 | margin-bottom: 20px; 72 | margin-left: -20px; 73 | } 74 | .nav-sidebar > li > a, .nav-sidebar > li > h2, .nav-sidebar > li > h3, h4 { 75 | padding-right: 20px; 76 | padding-left: 20px; 77 | } 78 | .nav-sidebar > .active > a { 79 | color: #fff; 80 | background-color: #428bca; 81 | } 82 | 83 | 84 | /* 85 | * Main content 86 | */ 87 | 88 | .main { 89 | padding: 20px; 90 | } 91 | @media (min-width: 768px) { 92 | .main { 93 | padding-right: 40px; 94 | padding-left: 40px; 95 | } 96 | } 97 | .main .page-header { 98 | margin-top: 0; 99 | } 100 | 101 | 102 | /* 103 | * Placeholder dashboard ideas 104 | */ 105 | 106 | .placeholders { 107 | margin-bottom: 30px; 108 | text-align: center; 109 | } 110 | .placeholders h4 { 111 | margin-bottom: 0; 112 | } 113 | .placeholder { 114 | margin-bottom: 20px; 115 | } 116 | .placeholder img { 117 | display: inline-block; 118 | border-radius: 50%; 119 | } 120 | -------------------------------------------------------------------------------- /public/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaneK/Matador/6f0d228b5878be29b6632c1099774c2dbdc0725f/public/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaneK/Matador/6f0d228b5878be29b6632c1099774c2dbdc0725f/public/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaneK/Matador/6f0d228b5878be29b6632c1099774c2dbdc0725f/public/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaneK/Matador/6f0d228b5878be29b6632c1099774c2dbdc0725f/public/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/img/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaneK/Matador/6f0d228b5878be29b6632c1099774c2dbdc0725f/public/img/close.png -------------------------------------------------------------------------------- /public/js/.jshintignore: -------------------------------------------------------------------------------- 1 | lib 2 | -------------------------------------------------------------------------------- /public/js/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "passfail": false, 3 | "maxerr": 100, 4 | 5 | "browser": true, 6 | "node": false, 7 | "rhino": false, 8 | "couch": false, 9 | "wsh": false, 10 | 11 | "jquery": false, 12 | "prototypejs": false, 13 | "mootools": false, 14 | "dojo": false, 15 | 16 | "debug": false, 17 | "devel": false, 18 | 19 | "es5": true, 20 | "strict": true, 21 | "globalstrict": true, 22 | 23 | "asi": false, 24 | "laxbreak": false, 25 | "bitwise": false, 26 | "boss": true, 27 | "curly": true, 28 | "eqeqeq": true, 29 | "eqnull": false, 30 | "evil": false, 31 | "expr": true, 32 | "forin": false, 33 | "immed": true, 34 | "latedef": false, 35 | "loopfunc": false, 36 | "noarg": false, 37 | "regexp": false, 38 | "regexdash": false, 39 | "scripturl": false, 40 | "shadow": false, 41 | "supernew": false, 42 | "undef": true, 43 | "validthis": false, 44 | "smarttabs": true, 45 | "proto": false, 46 | "onecase": false, 47 | "nonstandard": false, 48 | "multistr": false, 49 | "laxcomma": false, 50 | "lastsemic": false, 51 | "iterator": false, 52 | "funcscope": false, 53 | "esnext": false, 54 | 55 | "newcap": false, 56 | "noempty": false, 57 | "nonew": false, 58 | "nomen": false, 59 | "onevar": false, 60 | "plusplus": false, 61 | "sub": false, 62 | "trailing": false, 63 | "indent": 4, 64 | "white": true, 65 | 66 | "maxparams": 5, 67 | "maxdepth": 3, 68 | "maxstatements": 25, 69 | "maxcomplexity": 6, 70 | 71 | "predef": ["define", "requirejs"] 72 | } 73 | -------------------------------------------------------------------------------- /public/js/ChartHandler.js: -------------------------------------------------------------------------------- 1 | var ChartHandler = function () { 2 | var _self = this; 3 | _self.fn = { 4 | subscribe: function () { 5 | dataModel.memory.subscribe(function (data) { 6 | var chartData = _.map(data, function (memory) { 7 | return [memory.time, memory.memory.split(":")[0]]; 8 | }); 9 | $.plot("#memoryChart", [chartData], { 10 | xaxis: { 11 | mode: "time", 12 | timeformat: "%H:%M", 13 | tickSize: [1, "minute"] 14 | }, 15 | yaxis: { 16 | tickFormatter: function (val, axis) { 17 | if(!val) return 0; 18 | return Math.ceil(val/10000)/100 + " MB"; 19 | } 20 | } 21 | }); 22 | }); 23 | } 24 | }; 25 | 26 | return _self; 27 | } 28 | 29 | var chartHandler = new ChartHandler(); 30 | 31 | 32 | $(document).ready(function () { 33 | chartHandler.fn.subscribe(); 34 | }); -------------------------------------------------------------------------------- /public/js/DataModel.js: -------------------------------------------------------------------------------- 1 | var DataModel = function(){ 2 | var _self = this; 3 | 4 | //Page information 5 | _self.complete = ko.observable(null); 6 | _self.failed = ko.observable(null); 7 | _self.active = ko.observable(null); 8 | _self.pending = ko.observable(null); 9 | _self.delayed = ko.observable(null); 10 | _self.stuck = ko.observable(null); 11 | _self.queues = ko.observable(null); 12 | _self.keys = ko.observableArray([]); 13 | _self.memory = ko.observable({}); 14 | _self.peakMemory = ko.observable(""); 15 | 16 | _self.autoRefreshId = null; 17 | _self.fn = { 18 | refreshViewModel: function(force){ 19 | var pathname = window.location.pathname.replace(window.basepath, ''); 20 | var refreshUrl = window.basepath + '/api' + (pathname !== '/' ? pathname : ''); 21 | 22 | var refresh = function(){ 23 | $.getJSON(refreshUrl).done(function(data){ 24 | _self.complete(" ("+data.counts.complete+")"); 25 | _self.failed(" ("+data.counts.failed+")"); 26 | _self.active(" ("+data.counts.active+")"); 27 | _self.pending(" ("+data.counts.pending+")"); 28 | _self.delayed(" ("+data.counts.delayed+")"); 29 | _self.stuck(" ("+data.counts.stuck+")"); 30 | _self.keys(data.keys); 31 | if(data.memory){ 32 | _self.memory(data.memory.usage); 33 | _self.peakMemory(data.memory.peak.human); 34 | } 35 | }); 36 | }; 37 | if(force){ 38 | clearInterval(_self.autoRefreshId); 39 | refresh(); 40 | } 41 | _self.autoRefreshId = setInterval(refresh, 2500); 42 | } 43 | } 44 | 45 | return _self; 46 | } 47 | 48 | var dataModel = new DataModel(); 49 | -------------------------------------------------------------------------------- /public/js/RedisHandler.js: -------------------------------------------------------------------------------- 1 | var RedisHandler = function(){ 2 | var _self = this; 3 | 4 | _self.util = { 5 | notyConfirm: function(message, cb){ 6 | var buttons = [ 7 | {addClass: 'btn btn-primary', text: 'Ok', onClick: function ($noty) { $noty.close(); cb(); } }, 8 | {addClass: 'btn btn-danger', text: 'Cancel', onClick: function ($noty) { $noty.close(); } } 9 | ]; 10 | noty({text: message, type: 'alert', layout: 'center', buttons: buttons, modal: true}); 11 | }, 12 | handleAjaxResponse: function(ajaxResponse){ 13 | noty({text: ajaxResponse.message, type: ajaxResponse.success ? 'success' : 'error', timeout: 3500, layout: 'topRight'}); 14 | }, 15 | blockUI: function(){ 16 | $.blockUI({message: '

Please wait...

'}); 17 | } 18 | }; 19 | 20 | _self.fn = { 21 | deleteById: function(o){ 22 | var id = o.id; 23 | var type = o.type; 24 | _self.util.notyConfirm("Are you sure you want to delete the job of type "+ type + " with ID #"+id+"?", function(){ 25 | _self.util.blockUI(); 26 | $.getJSON(window.basepath + "/api/jobs/delete/id/"+type+"/"+id).done(function(response){ 27 | _self.util.handleAjaxResponse(response); 28 | dataModel.fn.refreshViewModel(true); 29 | }).always(function(){ 30 | $.unblockUI(); 31 | }); 32 | }); 33 | }, 34 | deleteByStatus: function(status, obj){ 35 | var queueName = obj.name; 36 | status = status.toLowerCase(); 37 | var statusDisplay = status; 38 | if(status === "pending"){ 39 | status = "wait"; 40 | statusDisplay = "pending"; 41 | } 42 | _self.util.notyConfirm("Are you sure you want to delete all jobs with the status "+statusDisplay+"?", function(){ 43 | _self.util.blockUI(); 44 | var targetUrl = null; 45 | if (queueName) { 46 | targetUrl = window.basepath + "/api/jobs/delete/status/"+status + "?queueName=" + queueName; 47 | } else { 48 | targetUrl = window.basepath + "/api/jobs/delete/status/"+status; 49 | } 50 | $.getJSON(targetUrl).done(function(response){ 51 | if(status !== statusDisplay && response.success){ 52 | response.message = response.message.replace(status, statusDisplay); 53 | } 54 | _self.util.handleAjaxResponse(response); 55 | dataModel.fn.refreshViewModel(true); 56 | }).always(function(){ 57 | $.unblockUI(); 58 | }); 59 | }); 60 | }, 61 | pendingById: function(o){ 62 | var id = o.id; 63 | var type = o.type; 64 | _self.util.notyConfirm("Are you sure you want make the job of type "+ type + " with ID #"+id+" pending? This will put this job in the queue to be run again.", function(){ 65 | _self.util.blockUI(); 66 | $.getJSON(window.basepath + "/api/jobs/pending/id/"+type+"/"+id).done(function(response){ 67 | _self.util.handleAjaxResponse(response); 68 | dataModel.fn.refreshViewModel(true); 69 | }).always(function(){ 70 | $.unblockUI(); 71 | }); 72 | }); 73 | }, 74 | infoById: function(o, e){ 75 | var id = o.id; 76 | var type = o.type; 77 | _self.util.blockUI(); 78 | $.getJSON(window.basepath + "/api/jobs/info/"+type+"/"+id).done(function(response){ 79 | if(response.success === false){ 80 | _self.util.handleAjaxResponse(response); 81 | }else{ 82 | var buttons = [ 83 | {addClass: 'btn btn-primary', text: 'Ok', onClick: function ($noty) { $noty.close(); } }, 84 | ]; 85 | 86 | var response = JSON.parse(JSON.stringify(response)); 87 | response.message.data = JSON.parse(response.message.data); 88 | var data = JSON.stringify(response.message.data, null, 2); 89 | var stacktrace = response.message.stacktrace; 90 | 91 | var message = '
Job ID: ' + id +
 92 |                         '\nType: ' + type +
 93 |                         '\nStatus: ' + o.status +
 94 |                         '\n\nData: ' + data;
 95 | 
 96 |                     if (stacktrace){
 97 |                         message = message + '\n\nStack Trace: \n' + stacktrace + '' + '
'; 98 | } else { 99 | message = message + ''; 100 | } 101 | 102 | var displayedNoty = noty({ 103 | text: message, 104 | type: 'alert', 105 | layout: 'center', 106 | buttons: buttons, 107 | modal: true, 108 | template: '
' 109 | }); 110 | 111 | displayedNoty.$message.parents('li').width("50vw"); 112 | displayedNoty.$message.parents('.i-am-new').css('left', '25vw'); 113 | } 114 | }).always(function(){ 115 | $.unblockUI(); 116 | }); 117 | }, 118 | pendingByStatus: function(status){ 119 | status = status.toLowerCase(); 120 | var statusDisplay = status; 121 | if(status === "pending"){ 122 | status = "wait"; 123 | statusDisplay = "pending"; 124 | } 125 | _self.util.notyConfirm("Are you sure you want to make all jobs with the status "+status+" pending? This will put all jobs with this status in the queue to be run again.", function(){ 126 | _self.util.blockUI(); 127 | $.getJSON(window.basepath + "/api/jobs/pending/status/"+status).done(function(response){ 128 | if(status !== statusDisplay && response.success){ 129 | response.message = response.message.replace(status, statusDisplay); 130 | } 131 | _self.util.handleAjaxResponse(response); 132 | dataModel.fn.refreshViewModel(true); 133 | }).always(function(){ 134 | $.unblockUI(); 135 | }); 136 | }); 137 | }, 138 | createJob: function(){ 139 | var data = $("#newjob").serializeArray(); 140 | var url = window.basepath + "/api/jobs/create"; 141 | $.ajax({ 142 | type:'POST', 143 | url:url, 144 | data: data, 145 | success: function(){ 146 | // clear form 147 | $('#newjob').trigger("reset"); 148 | $('.alert').html('').addClass('hidden'); 149 | }, 150 | error: function(response){ 151 | // display error 152 | $('.alert').html('Error! ' + response.responseText).removeClass('hidden'); 153 | } 154 | }); 155 | } 156 | }; 157 | 158 | return _self; 159 | }; 160 | 161 | var redisHandler = new RedisHandler(); 162 | -------------------------------------------------------------------------------- /public/js/flot/jquery.colorhelpers.js: -------------------------------------------------------------------------------- 1 | /* Plugin for jQuery for working with colors. 2 | * 3 | * Version 1.1. 4 | * 5 | * Inspiration from jQuery color animation plugin by John Resig. 6 | * 7 | * Released under the MIT license by Ole Laursen, October 2009. 8 | * 9 | * Examples: 10 | * 11 | * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() 12 | * var c = $.color.extract($("#mydiv"), 'background-color'); 13 | * console.log(c.r, c.g, c.b, c.a); 14 | * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" 15 | * 16 | * Note that .scale() and .add() return the same modified object 17 | * instead of making a new one. 18 | * 19 | * V. 1.1: Fix error handling so e.g. parsing an empty string does 20 | * produce a color rather than just crashing. 21 | */ 22 | 23 | (function($) { 24 | $.color = {}; 25 | 26 | // construct color object with some convenient chainable helpers 27 | $.color.make = function (r, g, b, a) { 28 | var o = {}; 29 | o.r = r || 0; 30 | o.g = g || 0; 31 | o.b = b || 0; 32 | o.a = a != null ? a : 1; 33 | 34 | o.add = function (c, d) { 35 | for (var i = 0; i < c.length; ++i) 36 | o[c.charAt(i)] += d; 37 | return o.normalize(); 38 | }; 39 | 40 | o.scale = function (c, f) { 41 | for (var i = 0; i < c.length; ++i) 42 | o[c.charAt(i)] *= f; 43 | return o.normalize(); 44 | }; 45 | 46 | o.toString = function () { 47 | if (o.a >= 1.0) { 48 | return "rgb("+[o.r, o.g, o.b].join(",")+")"; 49 | } else { 50 | return "rgba("+[o.r, o.g, o.b, o.a].join(",")+")"; 51 | } 52 | }; 53 | 54 | o.normalize = function () { 55 | function clamp(min, value, max) { 56 | return value < min ? min: (value > max ? max: value); 57 | } 58 | 59 | o.r = clamp(0, parseInt(o.r), 255); 60 | o.g = clamp(0, parseInt(o.g), 255); 61 | o.b = clamp(0, parseInt(o.b), 255); 62 | o.a = clamp(0, o.a, 1); 63 | return o; 64 | }; 65 | 66 | o.clone = function () { 67 | return $.color.make(o.r, o.b, o.g, o.a); 68 | }; 69 | 70 | return o.normalize(); 71 | } 72 | 73 | // extract CSS color property from element, going up in the DOM 74 | // if it's "transparent" 75 | $.color.extract = function (elem, css) { 76 | var c; 77 | 78 | do { 79 | c = elem.css(css).toLowerCase(); 80 | // keep going until we find an element that has color, or 81 | // we hit the body or root (have no parent) 82 | if (c != '' && c != 'transparent') 83 | break; 84 | elem = elem.parent(); 85 | } while (elem.length && !$.nodeName(elem.get(0), "body")); 86 | 87 | // catch Safari's way of signalling transparent 88 | if (c == "rgba(0, 0, 0, 0)") 89 | c = "transparent"; 90 | 91 | return $.color.parse(c); 92 | } 93 | 94 | // parse CSS color string (like "rgb(10, 32, 43)" or "#fff"), 95 | // returns color object, if parsing failed, you get black (0, 0, 96 | // 0) out 97 | $.color.parse = function (str) { 98 | var res, m = $.color.make; 99 | 100 | // Look for rgb(num,num,num) 101 | if (res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)) 102 | return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10)); 103 | 104 | // Look for rgba(num,num,num,num) 105 | if (res = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) 106 | return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4])); 107 | 108 | // Look for rgb(num%,num%,num%) 109 | if (res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)) 110 | return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55); 111 | 112 | // Look for rgba(num%,num%,num%,num) 113 | if (res = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) 114 | return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55, parseFloat(res[4])); 115 | 116 | // Look for #a0b1c2 117 | if (res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)) 118 | return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)); 119 | 120 | // Look for #fff 121 | if (res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)) 122 | return m(parseInt(res[1]+res[1], 16), parseInt(res[2]+res[2], 16), parseInt(res[3]+res[3], 16)); 123 | 124 | // Otherwise, we're most likely dealing with a named color 125 | var name = $.trim(str).toLowerCase(); 126 | if (name == "transparent") 127 | return m(255, 255, 255, 0); 128 | else { 129 | // default to black 130 | res = lookupColors[name] || [0, 0, 0]; 131 | return m(res[0], res[1], res[2]); 132 | } 133 | } 134 | 135 | var lookupColors = { 136 | aqua:[0,255,255], 137 | azure:[240,255,255], 138 | beige:[245,245,220], 139 | black:[0,0,0], 140 | blue:[0,0,255], 141 | brown:[165,42,42], 142 | cyan:[0,255,255], 143 | darkblue:[0,0,139], 144 | darkcyan:[0,139,139], 145 | darkgrey:[169,169,169], 146 | darkgreen:[0,100,0], 147 | darkkhaki:[189,183,107], 148 | darkmagenta:[139,0,139], 149 | darkolivegreen:[85,107,47], 150 | darkorange:[255,140,0], 151 | darkorchid:[153,50,204], 152 | darkred:[139,0,0], 153 | darksalmon:[233,150,122], 154 | darkviolet:[148,0,211], 155 | fuchsia:[255,0,255], 156 | gold:[255,215,0], 157 | green:[0,128,0], 158 | indigo:[75,0,130], 159 | khaki:[240,230,140], 160 | lightblue:[173,216,230], 161 | lightcyan:[224,255,255], 162 | lightgreen:[144,238,144], 163 | lightgrey:[211,211,211], 164 | lightpink:[255,182,193], 165 | lightyellow:[255,255,224], 166 | lime:[0,255,0], 167 | magenta:[255,0,255], 168 | maroon:[128,0,0], 169 | navy:[0,0,128], 170 | olive:[128,128,0], 171 | orange:[255,165,0], 172 | pink:[255,192,203], 173 | purple:[128,0,128], 174 | violet:[128,0,128], 175 | red:[255,0,0], 176 | silver:[192,192,192], 177 | white:[255,255,255], 178 | yellow:[255,255,0] 179 | }; 180 | })(jQuery); 181 | -------------------------------------------------------------------------------- /public/js/flot/jquery.colorhelpers.min.js: -------------------------------------------------------------------------------- 1 | (function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); -------------------------------------------------------------------------------- /public/js/flot/jquery.flot.canvas.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for drawing all elements of a plot on the canvas. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | Flot normally produces certain elements, like axis labels and the legend, using 7 | HTML elements. This permits greater interactivity and customization, and often 8 | looks better, due to cross-browser canvas text inconsistencies and limitations. 9 | 10 | It can also be desirable to render the plot entirely in canvas, particularly 11 | if the goal is to save it as an image, or if Flot is being used in a context 12 | where the HTML DOM does not exist, as is the case within Node.js. This plugin 13 | switches out Flot's standard drawing operations for canvas-only replacements. 14 | 15 | Currently the plugin supports only axis labels, but it will eventually allow 16 | every element of the plot to be rendered directly to canvas. 17 | 18 | The plugin supports these options: 19 | 20 | { 21 | canvas: boolean 22 | } 23 | 24 | The "canvas" option controls whether full canvas drawing is enabled, making it 25 | possible to toggle on and off. This is useful when a plot uses HTML text in the 26 | browser, but needs to redraw with canvas text when exporting as an image. 27 | 28 | */ 29 | 30 | (function($) { 31 | 32 | var options = { 33 | canvas: true 34 | }; 35 | 36 | var render, getTextInfo, addText; 37 | 38 | // Cache the prototype hasOwnProperty for faster access 39 | 40 | var hasOwnProperty = Object.prototype.hasOwnProperty; 41 | 42 | function init(plot, classes) { 43 | 44 | var Canvas = classes.Canvas; 45 | 46 | // We only want to replace the functions once; the second time around 47 | // we would just get our new function back. This whole replacing of 48 | // prototype functions is a disaster, and needs to be changed ASAP. 49 | 50 | if (render == null) { 51 | getTextInfo = Canvas.prototype.getTextInfo, 52 | addText = Canvas.prototype.addText, 53 | render = Canvas.prototype.render; 54 | } 55 | 56 | // Finishes rendering the canvas, including overlaid text 57 | 58 | Canvas.prototype.render = function() { 59 | 60 | if (!plot.getOptions().canvas) { 61 | return render.call(this); 62 | } 63 | 64 | var context = this.context, 65 | cache = this._textCache; 66 | 67 | // For each text layer, render elements marked as active 68 | 69 | context.save(); 70 | context.textBaseline = "middle"; 71 | 72 | for (var layerKey in cache) { 73 | if (hasOwnProperty.call(cache, layerKey)) { 74 | var layerCache = cache[layerKey]; 75 | for (var styleKey in layerCache) { 76 | if (hasOwnProperty.call(layerCache, styleKey)) { 77 | var styleCache = layerCache[styleKey], 78 | updateStyles = true; 79 | for (var key in styleCache) { 80 | if (hasOwnProperty.call(styleCache, key)) { 81 | 82 | var info = styleCache[key], 83 | positions = info.positions, 84 | lines = info.lines; 85 | 86 | // Since every element at this level of the cache have the 87 | // same font and fill styles, we can just change them once 88 | // using the values from the first element. 89 | 90 | if (updateStyles) { 91 | context.fillStyle = info.font.color; 92 | context.font = info.font.definition; 93 | updateStyles = false; 94 | } 95 | 96 | for (var i = 0, position; position = positions[i]; i++) { 97 | if (position.active) { 98 | for (var j = 0, line; line = position.lines[j]; j++) { 99 | context.fillText(lines[j].text, line[0], line[1]); 100 | } 101 | } else { 102 | positions.splice(i--, 1); 103 | } 104 | } 105 | 106 | if (positions.length == 0) { 107 | delete styleCache[key]; 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | context.restore(); 117 | }; 118 | 119 | // Creates (if necessary) and returns a text info object. 120 | // 121 | // When the canvas option is set, the object looks like this: 122 | // 123 | // { 124 | // width: Width of the text's bounding box. 125 | // height: Height of the text's bounding box. 126 | // positions: Array of positions at which this text is drawn. 127 | // lines: [{ 128 | // height: Height of this line. 129 | // widths: Width of this line. 130 | // text: Text on this line. 131 | // }], 132 | // font: { 133 | // definition: Canvas font property string. 134 | // color: Color of the text. 135 | // }, 136 | // } 137 | // 138 | // The positions array contains objects that look like this: 139 | // 140 | // { 141 | // active: Flag indicating whether the text should be visible. 142 | // lines: Array of [x, y] coordinates at which to draw the line. 143 | // x: X coordinate at which to draw the text. 144 | // y: Y coordinate at which to draw the text. 145 | // } 146 | 147 | Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { 148 | 149 | if (!plot.getOptions().canvas) { 150 | return getTextInfo.call(this, layer, text, font, angle, width); 151 | } 152 | 153 | var textStyle, layerCache, styleCache, info; 154 | 155 | // Cast the value to a string, in case we were given a number 156 | 157 | text = "" + text; 158 | 159 | // If the font is a font-spec object, generate a CSS definition 160 | 161 | if (typeof font === "object") { 162 | textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; 163 | } else { 164 | textStyle = font; 165 | } 166 | 167 | // Retrieve (or create) the cache for the text's layer and styles 168 | 169 | layerCache = this._textCache[layer]; 170 | 171 | if (layerCache == null) { 172 | layerCache = this._textCache[layer] = {}; 173 | } 174 | 175 | styleCache = layerCache[textStyle]; 176 | 177 | if (styleCache == null) { 178 | styleCache = layerCache[textStyle] = {}; 179 | } 180 | 181 | info = styleCache[text]; 182 | 183 | if (info == null) { 184 | 185 | var context = this.context; 186 | 187 | // If the font was provided as CSS, create a div with those 188 | // classes and examine it to generate a canvas font spec. 189 | 190 | if (typeof font !== "object") { 191 | 192 | var element = $("
 
") 193 | .css("position", "absolute") 194 | .addClass(typeof font === "string" ? font : null) 195 | .appendTo(this.getTextLayer(layer)); 196 | 197 | font = { 198 | lineHeight: element.height(), 199 | style: element.css("font-style"), 200 | variant: element.css("font-variant"), 201 | weight: element.css("font-weight"), 202 | family: element.css("font-family"), 203 | color: element.css("color") 204 | }; 205 | 206 | // Setting line-height to 1, without units, sets it equal 207 | // to the font-size, even if the font-size is abstract, 208 | // like 'smaller'. This enables us to read the real size 209 | // via the element's height, working around browsers that 210 | // return the literal 'smaller' value. 211 | 212 | font.size = element.css("line-height", 1).height(); 213 | 214 | element.remove(); 215 | } 216 | 217 | textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; 218 | 219 | // Create a new info object, initializing the dimensions to 220 | // zero so we can count them up line-by-line. 221 | 222 | info = styleCache[text] = { 223 | width: 0, 224 | height: 0, 225 | positions: [], 226 | lines: [], 227 | font: { 228 | definition: textStyle, 229 | color: font.color 230 | } 231 | }; 232 | 233 | context.save(); 234 | context.font = textStyle; 235 | 236 | // Canvas can't handle multi-line strings; break on various 237 | // newlines, including HTML brs, to build a list of lines. 238 | // Note that we could split directly on regexps, but IE < 9 is 239 | // broken; revisit when we drop IE 7/8 support. 240 | 241 | var lines = (text + "").replace(/
|\r\n|\r/g, "\n").split("\n"); 242 | 243 | for (var i = 0; i < lines.length; ++i) { 244 | 245 | var lineText = lines[i], 246 | measured = context.measureText(lineText); 247 | 248 | info.width = Math.max(measured.width, info.width); 249 | info.height += font.lineHeight; 250 | 251 | info.lines.push({ 252 | text: lineText, 253 | width: measured.width, 254 | height: font.lineHeight 255 | }); 256 | } 257 | 258 | context.restore(); 259 | } 260 | 261 | return info; 262 | }; 263 | 264 | // Adds a text string to the canvas text overlay. 265 | 266 | Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { 267 | 268 | if (!plot.getOptions().canvas) { 269 | return addText.call(this, layer, x, y, text, font, angle, width, halign, valign); 270 | } 271 | 272 | var info = this.getTextInfo(layer, text, font, angle, width), 273 | positions = info.positions, 274 | lines = info.lines; 275 | 276 | // Text is drawn with baseline 'middle', which we need to account 277 | // for by adding half a line's height to the y position. 278 | 279 | y += info.height / lines.length / 2; 280 | 281 | // Tweak the initial y-position to match vertical alignment 282 | 283 | if (valign == "middle") { 284 | y = Math.round(y - info.height / 2); 285 | } else if (valign == "bottom") { 286 | y = Math.round(y - info.height); 287 | } else { 288 | y = Math.round(y); 289 | } 290 | 291 | // FIXME: LEGACY BROWSER FIX 292 | // AFFECTS: Opera < 12.00 293 | 294 | // Offset the y coordinate, since Opera is off pretty 295 | // consistently compared to the other browsers. 296 | 297 | if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { 298 | y -= 2; 299 | } 300 | 301 | // Determine whether this text already exists at this position. 302 | // If so, mark it for inclusion in the next render pass. 303 | 304 | for (var i = 0, position; position = positions[i]; i++) { 305 | if (position.x == x && position.y == y) { 306 | position.active = true; 307 | return; 308 | } 309 | } 310 | 311 | // If the text doesn't exist at this position, create a new entry 312 | 313 | position = { 314 | active: true, 315 | lines: [], 316 | x: x, 317 | y: y 318 | }; 319 | 320 | positions.push(position); 321 | 322 | // Fill in the x & y positions of each line, adjusting them 323 | // individually for horizontal alignment. 324 | 325 | for (var i = 0, line; line = lines[i]; i++) { 326 | if (halign == "center") { 327 | position.lines.push([Math.round(x - line.width / 2), y]); 328 | } else if (halign == "right") { 329 | position.lines.push([Math.round(x - line.width), y]); 330 | } else { 331 | position.lines.push([Math.round(x), y]); 332 | } 333 | y += line.height; 334 | } 335 | }; 336 | } 337 | 338 | $.plot.plugins.push({ 339 | init: init, 340 | options: options, 341 | name: "canvas", 342 | version: "1.0" 343 | }); 344 | 345 | })(jQuery); 346 | -------------------------------------------------------------------------------- /public/js/flot/jquery.flot.canvas.min.js: -------------------------------------------------------------------------------- 1 | /* Javascript plotting library for jQuery, version 0.8.3. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | */ 7 | (function($){var options={canvas:true};var render,getTextInfo,addText;var hasOwnProperty=Object.prototype.hasOwnProperty;function init(plot,classes){var Canvas=classes.Canvas;if(render==null){getTextInfo=Canvas.prototype.getTextInfo,addText=Canvas.prototype.addText,render=Canvas.prototype.render}Canvas.prototype.render=function(){if(!plot.getOptions().canvas){return render.call(this)}var context=this.context,cache=this._textCache;context.save();context.textBaseline="middle";for(var layerKey in cache){if(hasOwnProperty.call(cache,layerKey)){var layerCache=cache[layerKey];for(var styleKey in layerCache){if(hasOwnProperty.call(layerCache,styleKey)){var styleCache=layerCache[styleKey],updateStyles=true;for(var key in styleCache){if(hasOwnProperty.call(styleCache,key)){var info=styleCache[key],positions=info.positions,lines=info.lines;if(updateStyles){context.fillStyle=info.font.color;context.font=info.font.definition;updateStyles=false}for(var i=0,position;position=positions[i];i++){if(position.active){for(var j=0,line;line=position.lines[j];j++){context.fillText(lines[j].text,line[0],line[1])}}else{positions.splice(i--,1)}}if(positions.length==0){delete styleCache[key]}}}}}}}context.restore()};Canvas.prototype.getTextInfo=function(layer,text,font,angle,width){if(!plot.getOptions().canvas){return getTextInfo.call(this,layer,text,font,angle,width)}var textStyle,layerCache,styleCache,info;text=""+text;if(typeof font==="object"){textStyle=font.style+" "+font.variant+" "+font.weight+" "+font.size+"px "+font.family}else{textStyle=font}layerCache=this._textCache[layer];if(layerCache==null){layerCache=this._textCache[layer]={}}styleCache=layerCache[textStyle];if(styleCache==null){styleCache=layerCache[textStyle]={}}info=styleCache[text];if(info==null){var context=this.context;if(typeof font!=="object"){var element=$("
 
").css("position","absolute").addClass(typeof font==="string"?font:null).appendTo(this.getTextLayer(layer));font={lineHeight:element.height(),style:element.css("font-style"),variant:element.css("font-variant"),weight:element.css("font-weight"),family:element.css("font-family"),color:element.css("color")};font.size=element.css("line-height",1).height();element.remove()}textStyle=font.style+" "+font.variant+" "+font.weight+" "+font.size+"px "+font.family;info=styleCache[text]={width:0,height:0,positions:[],lines:[],font:{definition:textStyle,color:font.color}};context.save();context.font=textStyle;var lines=(text+"").replace(/
|\r\n|\r/g,"\n").split("\n");for(var i=0;i index) 102 | index = categories[v]; 103 | 104 | return index + 1; 105 | } 106 | 107 | function categoriesTickGenerator(axis) { 108 | var res = []; 109 | for (var label in axis.categories) { 110 | var v = axis.categories[label]; 111 | if (v >= axis.min && v <= axis.max) 112 | res.push([v, label]); 113 | } 114 | 115 | res.sort(function (a, b) { return a[0] - b[0]; }); 116 | 117 | return res; 118 | } 119 | 120 | function setupCategoriesForAxis(series, axis, datapoints) { 121 | if (series[axis].options.mode != "categories") 122 | return; 123 | 124 | if (!series[axis].categories) { 125 | // parse options 126 | var c = {}, o = series[axis].options.categories || {}; 127 | if ($.isArray(o)) { 128 | for (var i = 0; i < o.length; ++i) 129 | c[o[i]] = i; 130 | } 131 | else { 132 | for (var v in o) 133 | c[v] = o[v]; 134 | } 135 | 136 | series[axis].categories = c; 137 | } 138 | 139 | // fix ticks 140 | if (!series[axis].options.ticks) 141 | series[axis].options.ticks = categoriesTickGenerator; 142 | 143 | transformPointsOnAxis(datapoints, axis, series[axis].categories); 144 | } 145 | 146 | function transformPointsOnAxis(datapoints, axis, categories) { 147 | // go through the points, transforming them 148 | var points = datapoints.points, 149 | ps = datapoints.pointsize, 150 | format = datapoints.format, 151 | formatColumn = axis.charAt(0), 152 | index = getNextIndex(categories); 153 | 154 | for (var i = 0; i < points.length; i += ps) { 155 | if (points[i] == null) 156 | continue; 157 | 158 | for (var m = 0; m < ps; ++m) { 159 | var val = points[i + m]; 160 | 161 | if (val == null || !format[m][formatColumn]) 162 | continue; 163 | 164 | if (!(val in categories)) { 165 | categories[val] = index; 166 | ++index; 167 | } 168 | 169 | points[i + m] = categories[val]; 170 | } 171 | } 172 | } 173 | 174 | function processDatapoints(plot, series, datapoints) { 175 | setupCategoriesForAxis(series, "xaxis", datapoints); 176 | setupCategoriesForAxis(series, "yaxis", datapoints); 177 | } 178 | 179 | function init(plot) { 180 | plot.hooks.processRawData.push(processRawData); 181 | plot.hooks.processDatapoints.push(processDatapoints); 182 | } 183 | 184 | $.plot.plugins.push({ 185 | init: init, 186 | options: options, 187 | name: 'categories', 188 | version: '1.0' 189 | }); 190 | })(jQuery); 191 | -------------------------------------------------------------------------------- /public/js/flot/jquery.flot.categories.min.js: -------------------------------------------------------------------------------- 1 | /* Javascript plotting library for jQuery, version 0.8.3. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | */ 7 | (function($){var options={xaxis:{categories:null},yaxis:{categories:null}};function processRawData(plot,series,data,datapoints){var xCategories=series.xaxis.options.mode=="categories",yCategories=series.yaxis.options.mode=="categories";if(!(xCategories||yCategories))return;var format=datapoints.format;if(!format){var s=series;format=[];format.push({x:true,number:true,required:true});format.push({y:true,number:true,required:true});if(s.bars.show||s.lines.show&&s.lines.fill){var autoscale=!!(s.bars.show&&s.bars.zero||s.lines.show&&s.lines.zero);format.push({y:true,number:true,required:false,defaultValue:0,autoscale:autoscale});if(s.bars.horizontal){delete format[format.length-1].y;format[format.length-1].x=true}}datapoints.format=format}for(var m=0;mindex)index=categories[v];return index+1}function categoriesTickGenerator(axis){var res=[];for(var label in axis.categories){var v=axis.categories[label];if(v>=axis.min&&v<=axis.max)res.push([v,label])}res.sort(function(a,b){return a[0]-b[0]});return res}function setupCategoriesForAxis(series,axis,datapoints){if(series[axis].options.mode!="categories")return;if(!series[axis].categories){var c={},o=series[axis].options.categories||{};if($.isArray(o)){for(var i=0;iax[1].max||yax[0].max)continue;if(err[e].err=="y")if(x>ax[0].max||xax[1].max)continue;var drawUpper=true,drawLower=true;if(upper>minmax[1]){drawUpper=false;upper=minmax[1]}if(lower0&&sw>0){var w=sw/2;ctx.lineWidth=w;ctx.strokeStyle="rgba(0,0,0,0.1)";drawError(ctx,err[e],x,y,upper,lower,drawUpper,drawLower,radius,w+w/2,minmax);ctx.strokeStyle="rgba(0,0,0,0.2)";drawError(ctx,err[e],x,y,upper,lower,drawUpper,drawLower,radius,w/2,minmax)}ctx.strokeStyle=err[e].color?err[e].color:s.color;ctx.lineWidth=lw;drawError(ctx,err[e],x,y,upper,lower,drawUpper,drawLower,radius,0,minmax)}}}}function drawError(ctx,err,x,y,upper,lower,drawUpper,drawLower,radius,offset,minmax){y+=offset;upper+=offset;lower+=offset;if(err.err=="x"){if(upper>x+radius)drawPath(ctx,[[upper,y],[Math.max(x+radius,minmax[0]),y]]);else drawUpper=false;if(lowery+radius)drawPath(ctx,[[x,Math.max(y+radius,minmax[1])],[x,lower]]);else drawLower=false}radius=err.radius!=null?err.radius:radius;if(drawUpper){if(err.upperCap=="-"){if(err.err=="x")drawPath(ctx,[[upper,y-radius],[upper,y+radius]]);else drawPath(ctx,[[x-radius,upper],[x+radius,upper]])}else if($.isFunction(err.upperCap)){if(err.err=="x")err.upperCap(ctx,upper,y,radius);else err.upperCap(ctx,x,upper,radius)}}if(drawLower){if(err.lowerCap=="-"){if(err.err=="x")drawPath(ctx,[[lower,y-radius],[lower,y+radius]]);else drawPath(ctx,[[x-radius,lower],[x+radius,lower]])}else if($.isFunction(err.lowerCap)){if(err.err=="x")err.lowerCap(ctx,lower,y,radius);else err.lowerCap(ctx,x,lower,radius)}}}function drawPath(ctx,pts){ctx.beginPath();ctx.moveTo(pts[0][0],pts[0][1]);for(var p=1;p= allseries.length ) { 54 | return null; 55 | } 56 | return allseries[ s.fillBetween ]; 57 | } 58 | 59 | return null; 60 | } 61 | 62 | function computeFillBottoms( plot, s, datapoints ) { 63 | 64 | if ( s.fillBetween == null ) { 65 | return; 66 | } 67 | 68 | var other = findBottomSeries( s, plot.getData() ); 69 | 70 | if ( !other ) { 71 | return; 72 | } 73 | 74 | var ps = datapoints.pointsize, 75 | points = datapoints.points, 76 | otherps = other.datapoints.pointsize, 77 | otherpoints = other.datapoints.points, 78 | newpoints = [], 79 | px, py, intery, qx, qy, bottom, 80 | withlines = s.lines.show, 81 | withbottom = ps > 2 && datapoints.format[2].y, 82 | withsteps = withlines && s.lines.steps, 83 | fromgap = true, 84 | i = 0, 85 | j = 0, 86 | l, m; 87 | 88 | while ( true ) { 89 | 90 | if ( i >= points.length ) { 91 | break; 92 | } 93 | 94 | l = newpoints.length; 95 | 96 | if ( points[ i ] == null ) { 97 | 98 | // copy gaps 99 | 100 | for ( m = 0; m < ps; ++m ) { 101 | newpoints.push( points[ i + m ] ); 102 | } 103 | 104 | i += ps; 105 | 106 | } else if ( j >= otherpoints.length ) { 107 | 108 | // for lines, we can't use the rest of the points 109 | 110 | if ( !withlines ) { 111 | for ( m = 0; m < ps; ++m ) { 112 | newpoints.push( points[ i + m ] ); 113 | } 114 | } 115 | 116 | i += ps; 117 | 118 | } else if ( otherpoints[ j ] == null ) { 119 | 120 | // oops, got a gap 121 | 122 | for ( m = 0; m < ps; ++m ) { 123 | newpoints.push( null ); 124 | } 125 | 126 | fromgap = true; 127 | j += otherps; 128 | 129 | } else { 130 | 131 | // cases where we actually got two points 132 | 133 | px = points[ i ]; 134 | py = points[ i + 1 ]; 135 | qx = otherpoints[ j ]; 136 | qy = otherpoints[ j + 1 ]; 137 | bottom = 0; 138 | 139 | if ( px === qx ) { 140 | 141 | for ( m = 0; m < ps; ++m ) { 142 | newpoints.push( points[ i + m ] ); 143 | } 144 | 145 | //newpoints[ l + 1 ] += qy; 146 | bottom = qy; 147 | 148 | i += ps; 149 | j += otherps; 150 | 151 | } else if ( px > qx ) { 152 | 153 | // we got past point below, might need to 154 | // insert interpolated extra point 155 | 156 | if ( withlines && i > 0 && points[ i - ps ] != null ) { 157 | intery = py + ( points[ i - ps + 1 ] - py ) * ( qx - px ) / ( points[ i - ps ] - px ); 158 | newpoints.push( qx ); 159 | newpoints.push( intery ); 160 | for ( m = 2; m < ps; ++m ) { 161 | newpoints.push( points[ i + m ] ); 162 | } 163 | bottom = qy; 164 | } 165 | 166 | j += otherps; 167 | 168 | } else { // px < qx 169 | 170 | // if we come from a gap, we just skip this point 171 | 172 | if ( fromgap && withlines ) { 173 | i += ps; 174 | continue; 175 | } 176 | 177 | for ( m = 0; m < ps; ++m ) { 178 | newpoints.push( points[ i + m ] ); 179 | } 180 | 181 | // we might be able to interpolate a point below, 182 | // this can give us a better y 183 | 184 | if ( withlines && j > 0 && otherpoints[ j - otherps ] != null ) { 185 | bottom = qy + ( otherpoints[ j - otherps + 1 ] - qy ) * ( px - qx ) / ( otherpoints[ j - otherps ] - qx ); 186 | } 187 | 188 | //newpoints[l + 1] += bottom; 189 | 190 | i += ps; 191 | } 192 | 193 | fromgap = false; 194 | 195 | if ( l !== newpoints.length && withbottom ) { 196 | newpoints[ l + 2 ] = bottom; 197 | } 198 | } 199 | 200 | // maintain the line steps invariant 201 | 202 | if ( withsteps && l !== newpoints.length && l > 0 && 203 | newpoints[ l ] !== null && 204 | newpoints[ l ] !== newpoints[ l - ps ] && 205 | newpoints[ l + 1 ] !== newpoints[ l - ps + 1 ] ) { 206 | for (m = 0; m < ps; ++m) { 207 | newpoints[ l + ps + m ] = newpoints[ l + m ]; 208 | } 209 | newpoints[ l + 1 ] = newpoints[ l - ps + 1 ]; 210 | } 211 | } 212 | 213 | datapoints.points = newpoints; 214 | } 215 | 216 | plot.hooks.processDatapoints.push( computeFillBottoms ); 217 | } 218 | 219 | $.plot.plugins.push({ 220 | init: init, 221 | options: options, 222 | name: "fillbetween", 223 | version: "1.0" 224 | }); 225 | 226 | })(jQuery); 227 | -------------------------------------------------------------------------------- /public/js/flot/jquery.flot.fillbetween.min.js: -------------------------------------------------------------------------------- 1 | /* Javascript plotting library for jQuery, version 0.8.3. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | */ 7 | (function($){var options={series:{fillBetween:null}};function init(plot){function findBottomSeries(s,allseries){var i;for(i=0;i=allseries.length){return null}return allseries[s.fillBetween]}return null}function computeFillBottoms(plot,s,datapoints){if(s.fillBetween==null){return}var other=findBottomSeries(s,plot.getData());if(!other){return}var ps=datapoints.pointsize,points=datapoints.points,otherps=other.datapoints.pointsize,otherpoints=other.datapoints.points,newpoints=[],px,py,intery,qx,qy,bottom,withlines=s.lines.show,withbottom=ps>2&&datapoints.format[2].y,withsteps=withlines&&s.lines.steps,fromgap=true,i=0,j=0,l,m;while(true){if(i>=points.length){break}l=newpoints.length;if(points[i]==null){for(m=0;m=otherpoints.length){if(!withlines){for(m=0;mqx){if(withlines&&i>0&&points[i-ps]!=null){intery=py+(points[i-ps+1]-py)*(qx-px)/(points[i-ps]-px);newpoints.push(qx);newpoints.push(intery);for(m=2;m0&&otherpoints[j-otherps]!=null){bottom=qy+(otherpoints[j-otherps+1]-qy)*(px-qx)/(otherpoints[j-otherps]-qx)}i+=ps}fromgap=false;if(l!==newpoints.length&&withbottom){newpoints[l+2]=bottom}}if(withsteps&&l!==newpoints.length&&l>0&&newpoints[l]!==null&&newpoints[l]!==newpoints[l-ps]&&newpoints[l+1]!==newpoints[l-ps+1]){for(m=0;m').load(handler).error(handler).attr('src', url); 115 | }); 116 | }; 117 | 118 | function drawSeries(plot, ctx, series) { 119 | var plotOffset = plot.getPlotOffset(); 120 | 121 | if (!series.images || !series.images.show) 122 | return; 123 | 124 | var points = series.datapoints.points, 125 | ps = series.datapoints.pointsize; 126 | 127 | for (var i = 0; i < points.length; i += ps) { 128 | var img = points[i], 129 | x1 = points[i + 1], y1 = points[i + 2], 130 | x2 = points[i + 3], y2 = points[i + 4], 131 | xaxis = series.xaxis, yaxis = series.yaxis, 132 | tmp; 133 | 134 | // actually we should check img.complete, but it 135 | // appears to be a somewhat unreliable indicator in 136 | // IE6 (false even after load event) 137 | if (!img || img.width <= 0 || img.height <= 0) 138 | continue; 139 | 140 | if (x1 > x2) { 141 | tmp = x2; 142 | x2 = x1; 143 | x1 = tmp; 144 | } 145 | if (y1 > y2) { 146 | tmp = y2; 147 | y2 = y1; 148 | y1 = tmp; 149 | } 150 | 151 | // if the anchor is at the center of the pixel, expand the 152 | // image by 1/2 pixel in each direction 153 | if (series.images.anchor == "center") { 154 | tmp = 0.5 * (x2-x1) / (img.width - 1); 155 | x1 -= tmp; 156 | x2 += tmp; 157 | tmp = 0.5 * (y2-y1) / (img.height - 1); 158 | y1 -= tmp; 159 | y2 += tmp; 160 | } 161 | 162 | // clip 163 | if (x1 == x2 || y1 == y2 || 164 | x1 >= xaxis.max || x2 <= xaxis.min || 165 | y1 >= yaxis.max || y2 <= yaxis.min) 166 | continue; 167 | 168 | var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height; 169 | if (x1 < xaxis.min) { 170 | sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1); 171 | x1 = xaxis.min; 172 | } 173 | 174 | if (x2 > xaxis.max) { 175 | sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1); 176 | x2 = xaxis.max; 177 | } 178 | 179 | if (y1 < yaxis.min) { 180 | sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1); 181 | y1 = yaxis.min; 182 | } 183 | 184 | if (y2 > yaxis.max) { 185 | sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1); 186 | y2 = yaxis.max; 187 | } 188 | 189 | x1 = xaxis.p2c(x1); 190 | x2 = xaxis.p2c(x2); 191 | y1 = yaxis.p2c(y1); 192 | y2 = yaxis.p2c(y2); 193 | 194 | // the transformation may have swapped us 195 | if (x1 > x2) { 196 | tmp = x2; 197 | x2 = x1; 198 | x1 = tmp; 199 | } 200 | if (y1 > y2) { 201 | tmp = y2; 202 | y2 = y1; 203 | y1 = tmp; 204 | } 205 | 206 | tmp = ctx.globalAlpha; 207 | ctx.globalAlpha *= series.images.alpha; 208 | ctx.drawImage(img, 209 | sx1, sy1, sx2 - sx1, sy2 - sy1, 210 | x1 + plotOffset.left, y1 + plotOffset.top, 211 | x2 - x1, y2 - y1); 212 | ctx.globalAlpha = tmp; 213 | } 214 | } 215 | 216 | function processRawData(plot, series, data, datapoints) { 217 | if (!series.images.show) 218 | return; 219 | 220 | // format is Image, x1, y1, x2, y2 (opposite corners) 221 | datapoints.format = [ 222 | { required: true }, 223 | { x: true, number: true, required: true }, 224 | { y: true, number: true, required: true }, 225 | { x: true, number: true, required: true }, 226 | { y: true, number: true, required: true } 227 | ]; 228 | } 229 | 230 | function init(plot) { 231 | plot.hooks.processRawData.push(processRawData); 232 | plot.hooks.drawSeries.push(drawSeries); 233 | } 234 | 235 | $.plot.plugins.push({ 236 | init: init, 237 | options: options, 238 | name: 'image', 239 | version: '1.1' 240 | }); 241 | })(jQuery); 242 | -------------------------------------------------------------------------------- /public/js/flot/jquery.flot.image.min.js: -------------------------------------------------------------------------------- 1 | /* Javascript plotting library for jQuery, version 0.8.3. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | */ 7 | (function($){var options={series:{images:{show:false,alpha:1,anchor:"corner"}}};$.plot.image={};$.plot.image.loadDataImages=function(series,options,callback){var urls=[],points=[];var defaultShow=options.series.images.show;$.each(series,function(i,s){if(!(defaultShow||s.images.show))return;if(s.data)s=s.data;$.each(s,function(i,p){if(typeof p[0]=="string"){urls.push(p[0]);points.push(p)}})});$.plot.image.load(urls,function(loadedImages){$.each(points,function(i,p){var url=p[0];if(loadedImages[url])p[0]=loadedImages[url]});callback()})};$.plot.image.load=function(urls,callback){var missing=urls.length,loaded={};if(missing==0)callback({});$.each(urls,function(i,url){var handler=function(){--missing;loaded[url]=this;if(missing==0)callback(loaded)};$("").load(handler).error(handler).attr("src",url)})};function drawSeries(plot,ctx,series){var plotOffset=plot.getPlotOffset();if(!series.images||!series.images.show)return;var points=series.datapoints.points,ps=series.datapoints.pointsize;for(var i=0;ix2){tmp=x2;x2=x1;x1=tmp}if(y1>y2){tmp=y2;y2=y1;y1=tmp}if(series.images.anchor=="center"){tmp=.5*(x2-x1)/(img.width-1);x1-=tmp;x2+=tmp;tmp=.5*(y2-y1)/(img.height-1);y1-=tmp;y2+=tmp}if(x1==x2||y1==y2||x1>=xaxis.max||x2<=xaxis.min||y1>=yaxis.max||y2<=yaxis.min)continue;var sx1=0,sy1=0,sx2=img.width,sy2=img.height;if(x1xaxis.max){sx2+=(sx2-sx1)*(xaxis.max-x2)/(x2-x1);x2=xaxis.max}if(y1yaxis.max){sy1+=(sy1-sy2)*(yaxis.max-y2)/(y2-y1);y2=yaxis.max}x1=xaxis.p2c(x1);x2=xaxis.p2c(x2);y1=yaxis.p2c(y1);y2=yaxis.p2c(y2);if(x1>x2){tmp=x2;x2=x1;x1=tmp}if(y1>y2){tmp=y2;y2=y1;y1=tmp}tmp=ctx.globalAlpha;ctx.globalAlpha*=series.images.alpha;ctx.drawImage(img,sx1,sy1,sx2-sx1,sy2-sy1,x1+plotOffset.left,y1+plotOffset.top,x2-x1,y2-y1);ctx.globalAlpha=tmp}}function processRawData(plot,series,data,datapoints){if(!series.images.show)return;datapoints.format=[{required:true},{x:true,number:true,required:true},{y:true,number:true,required:true},{x:true,number:true,required:true},{y:true,number:true,required:true}]}function init(plot){plot.hooks.processRawData.push(processRawData);plot.hooks.drawSeries.push(drawSeries)}$.plot.plugins.push({init:init,options:options,name:"image",version:"1.1"})})(jQuery); -------------------------------------------------------------------------------- /public/js/flot/jquery.flot.navigate.min.js: -------------------------------------------------------------------------------- 1 | /* Javascript plotting library for jQuery, version 0.8.3. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | */ 7 | (function(a){function e(h){var k,j=this,l=h.data||{};if(l.elem)j=h.dragTarget=l.elem,h.dragProxy=d.proxy||j,h.cursorOffsetX=l.pageX-l.left,h.cursorOffsetY=l.pageY-l.top,h.offsetX=h.pageX-h.cursorOffsetX,h.offsetY=h.pageY-h.cursorOffsetY;else if(d.dragging||l.which>0&&h.which!=l.which||a(h.target).is(l.not))return;switch(h.type){case"mousedown":return a.extend(l,a(j).offset(),{elem:j,target:h.target,pageX:h.pageX,pageY:h.pageY}),b.add(document,"mousemove mouseup",e,l),i(j,!1),d.dragging=null,!1;case!d.dragging&&"mousemove":if(g(h.pageX-l.pageX)+g(h.pageY-l.pageY)max){var tmp=min;min=max;max=tmp}if(pr){if(pr[0]!=null&&minpr[1]){max=pr[1]}}var range=max-min;if(zr&&(zr[0]!=null&&range1||zr[1]!=null&&range>zr[1]&&amount<1))return;opts.min=min;opts.max=max});plot.setupGrid();plot.draw();if(!args.preventEvent)plot.getPlaceholder().trigger("plotzoom",[plot,args])};plot.pan=function(args){var delta={x:+args.left,y:+args.top};if(isNaN(delta.x))delta.x=0;if(isNaN(delta.y))delta.y=0;$.each(plot.getAxes(),function(_,axis){var opts=axis.options,min,max,d=delta[axis.direction];min=axis.c2p(axis.p2c(axis.min)+d),max=axis.c2p(axis.p2c(axis.max)+d);var pr=opts.panRange;if(pr===false)return;if(pr){if(pr[0]!=null&&pr[0]>min){d=pr[0]-min;min+=d;max+=d}if(pr[1]!=null&&pr[1]1){options.series.pie.tilt=1}else if(options.series.pie.tilt<0){options.series.pie.tilt=0}}});plot.hooks.bindEvents.push(function(plot,eventHolder){var options=plot.getOptions();if(options.series.pie.show){if(options.grid.hoverable){eventHolder.unbind("mousemove").mousemove(onMouseMove)}if(options.grid.clickable){eventHolder.unbind("click").click(onClick)}}});plot.hooks.processDatapoints.push(function(plot,series,data,datapoints){var options=plot.getOptions();if(options.series.pie.show){processDatapoints(plot,series,data,datapoints)}});plot.hooks.drawOverlay.push(function(plot,octx){var options=plot.getOptions();if(options.series.pie.show){drawOverlay(plot,octx)}});plot.hooks.draw.push(function(plot,newCtx){var options=plot.getOptions();if(options.series.pie.show){draw(plot,newCtx)}});function processDatapoints(plot,series,datapoints){if(!processed){processed=true;canvas=plot.getCanvas();target=$(canvas).parent();options=plot.getOptions();plot.setData(combine(plot.getData()))}}function combine(data){var total=0,combined=0,numCombined=0,color=options.series.pie.combine.color,newdata=[];for(var i=0;ioptions.series.pie.combine.threshold){newdata.push($.extend(data[i],{data:[[1,value]],color:data[i].color,label:data[i].label,angle:value*Math.PI*2/total,percent:value/(total/100)}))}}if(numCombined>1){newdata.push({data:[[1,combined]],color:color,label:options.series.pie.combine.label,angle:combined*Math.PI*2/total,percent:combined/(total/100)})}return newdata}function draw(plot,newCtx){if(!target){return}var canvasWidth=plot.getPlaceholder().width(),canvasHeight=plot.getPlaceholder().height(),legendWidth=target.children().filter(".legend").children().width()||0;ctx=newCtx;processed=false;maxRadius=Math.min(canvasWidth,canvasHeight/options.series.pie.tilt)/2;centerTop=canvasHeight/2+options.series.pie.offset.top;centerLeft=canvasWidth/2;if(options.series.pie.offset.left=="auto"){if(options.legend.position.match("w")){centerLeft+=legendWidth/2}else{centerLeft-=legendWidth/2}if(centerLeftcanvasWidth-maxRadius){centerLeft=canvasWidth-maxRadius}}else{centerLeft+=options.series.pie.offset.left}var slices=plot.getData(),attempts=0;do{if(attempts>0){maxRadius*=REDRAW_SHRINK}attempts+=1;clear();if(options.series.pie.tilt<=.8){drawShadow()}}while(!drawPie()&&attempts=REDRAW_ATTEMPTS){clear();target.prepend("
Could not draw pie with labels contained inside canvas
")}if(plot.setSeries&&plot.insertLegend){plot.setSeries(slices);plot.insertLegend()}function clear(){ctx.clearRect(0,0,canvasWidth,canvasHeight);target.children().filter(".pieLabel, .pieLabelBackground").remove()}function drawShadow(){var shadowLeft=options.series.pie.shadow.left;var shadowTop=options.series.pie.shadow.top;var edge=10;var alpha=options.series.pie.shadow.alpha;var radius=options.series.pie.radius>1?options.series.pie.radius:maxRadius*options.series.pie.radius;if(radius>=canvasWidth/2-shadowLeft||radius*options.series.pie.tilt>=canvasHeight/2-shadowTop||radius<=edge){return}ctx.save();ctx.translate(shadowLeft,shadowTop);ctx.globalAlpha=alpha;ctx.fillStyle="#000";ctx.translate(centerLeft,centerTop);ctx.scale(1,options.series.pie.tilt);for(var i=1;i<=edge;i++){ctx.beginPath();ctx.arc(0,0,radius,0,Math.PI*2,false);ctx.fill();radius-=i}ctx.restore()}function drawPie(){var startAngle=Math.PI*options.series.pie.startAngle;var radius=options.series.pie.radius>1?options.series.pie.radius:maxRadius*options.series.pie.radius;ctx.save();ctx.translate(centerLeft,centerTop);ctx.scale(1,options.series.pie.tilt);ctx.save();var currentAngle=startAngle;for(var i=0;i0){ctx.save();ctx.lineWidth=options.series.pie.stroke.width;currentAngle=startAngle;for(var i=0;i1e-9){ctx.moveTo(0,0)}ctx.arc(0,0,radius,currentAngle,currentAngle+angle/2,false);ctx.arc(0,0,radius,currentAngle+angle/2,currentAngle+angle,false);ctx.closePath();currentAngle+=angle;if(fill){ctx.fill()}else{ctx.stroke()}}function drawLabels(){var currentAngle=startAngle;var radius=options.series.pie.label.radius>1?options.series.pie.label.radius:maxRadius*options.series.pie.label.radius;for(var i=0;i=options.series.pie.label.threshold*100){if(!drawLabel(slices[i],currentAngle,i)){return false}}currentAngle+=slices[i].angle}return true;function drawLabel(slice,startAngle,index){if(slice.data[0][1]==0){return true}var lf=options.legend.labelFormatter,text,plf=options.series.pie.label.formatter;if(lf){text=lf(slice.label,slice)}else{text=slice.label}if(plf){text=plf(text,slice)}var halfAngle=(startAngle+slice.angle+startAngle)/2;var x=centerLeft+Math.round(Math.cos(halfAngle)*radius);var y=centerTop+Math.round(Math.sin(halfAngle)*radius)*options.series.pie.tilt;var html=""+text+"";target.append(html);var label=target.children("#pieLabel"+index);var labelTop=y-label.height()/2;var labelLeft=x-label.width()/2;label.css("top",labelTop);label.css("left",labelLeft);if(0-labelTop>0||0-labelLeft>0||canvasHeight-(labelTop+label.height())<0||canvasWidth-(labelLeft+label.width())<0){return false}if(options.series.pie.label.background.opacity!=0){var c=options.series.pie.label.background.color;if(c==null){c=slice.color}var pos="top:"+labelTop+"px;left:"+labelLeft+"px;";$("
").css("opacity",options.series.pie.label.background.opacity).insertBefore(label)}return true}}}}function drawDonutHole(layer){if(options.series.pie.innerRadius>0){layer.save();var innerRadius=options.series.pie.innerRadius>1?options.series.pie.innerRadius:maxRadius*options.series.pie.innerRadius;layer.globalCompositeOperation="destination-out";layer.beginPath();layer.fillStyle=options.series.pie.stroke.color;layer.arc(0,0,innerRadius,0,Math.PI*2,false);layer.fill();layer.closePath();layer.restore();layer.save();layer.beginPath();layer.strokeStyle=options.series.pie.stroke.color;layer.arc(0,0,innerRadius,0,Math.PI*2,false);layer.stroke();layer.closePath();layer.restore()}}function isPointInPoly(poly,pt){for(var c=false,i=-1,l=poly.length,j=l-1;++i1?options.series.pie.radius:maxRadius*options.series.pie.radius,x,y;for(var i=0;i1?options.series.pie.radius:maxRadius*options.series.pie.radius;octx.save();octx.translate(centerLeft,centerTop);octx.scale(1,options.series.pie.tilt);for(var i=0;i1e-9){octx.moveTo(0,0)}octx.arc(0,0,radius,series.startAngle,series.startAngle+series.angle/2,false);octx.arc(0,0,radius,series.startAngle+series.angle/2,series.startAngle+series.angle,false);octx.closePath();octx.fill()}}}var options={series:{pie:{show:false,radius:"auto",innerRadius:0,startAngle:3/2,tilt:1,shadow:{left:5,top:15,alpha:.02},offset:{top:0,left:"auto"},stroke:{color:"#fff",width:1},label:{show:"auto",formatter:function(label,slice){return"
"+label+"
"+Math.round(slice.percent)+"%
"},radius:1,background:{color:null,opacity:0},threshold:0},combine:{threshold:-1,color:null,label:"Other"},highlight:{opacity:.5}}}};$.plot.plugins.push({init:init,options:options,name:"pie",version:"1.1"})})(jQuery); -------------------------------------------------------------------------------- /public/js/flot/jquery.flot.resize.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for automatically redrawing plots as the placeholder resizes. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | It works by listening for changes on the placeholder div (through the jQuery 7 | resize event plugin) - if the size changes, it will redraw the plot. 8 | 9 | There are no options. If you need to disable the plugin for some plots, you 10 | can just fix the size of their placeholders. 11 | 12 | */ 13 | 14 | /* Inline dependency: 15 | * jQuery resize event - v1.1 - 3/14/2010 16 | * http://benalman.com/projects/jquery-resize-plugin/ 17 | * 18 | * Copyright (c) 2010 "Cowboy" Ben Alman 19 | * Dual licensed under the MIT and GPL licenses. 20 | * http://benalman.com/about/license/ 21 | */ 22 | (function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this); 23 | 24 | (function ($) { 25 | var options = { }; // no options 26 | 27 | function init(plot) { 28 | function onResize() { 29 | var placeholder = plot.getPlaceholder(); 30 | 31 | // somebody might have hidden us and we can't plot 32 | // when we don't have the dimensions 33 | if (placeholder.width() == 0 || placeholder.height() == 0) 34 | return; 35 | 36 | plot.resize(); 37 | plot.setupGrid(); 38 | plot.draw(); 39 | } 40 | 41 | function bindEvents(plot, eventHolder) { 42 | plot.getPlaceholder().resize(onResize); 43 | } 44 | 45 | function shutdown(plot, eventHolder) { 46 | plot.getPlaceholder().unbind("resize", onResize); 47 | } 48 | 49 | plot.hooks.bindEvents.push(bindEvents); 50 | plot.hooks.shutdown.push(shutdown); 51 | } 52 | 53 | $.plot.plugins.push({ 54 | init: init, 55 | options: options, 56 | name: 'resize', 57 | version: '1.0' 58 | }); 59 | })(jQuery); 60 | -------------------------------------------------------------------------------- /public/js/flot/jquery.flot.resize.min.js: -------------------------------------------------------------------------------- 1 | /* Javascript plotting library for jQuery, version 0.8.3. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | */ 7 | (function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this);(function($){var options={};function init(plot){function onResize(){var placeholder=plot.getPlaceholder();if(placeholder.width()==0||placeholder.height()==0)return;plot.resize();plot.setupGrid();plot.draw()}function bindEvents(plot,eventHolder){plot.getPlaceholder().resize(onResize)}function shutdown(plot,eventHolder){plot.getPlaceholder().unbind("resize",onResize)}plot.hooks.bindEvents.push(bindEvents);plot.hooks.shutdown.push(shutdown)}$.plot.plugins.push({init:init,options:options,name:"resize",version:"1.0"})})(jQuery); -------------------------------------------------------------------------------- /public/js/flot/jquery.flot.selection.min.js: -------------------------------------------------------------------------------- 1 | /* Javascript plotting library for jQuery, version 0.8.3. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | */ 7 | (function($){function init(plot){var selection={first:{x:-1,y:-1},second:{x:-1,y:-1},show:false,active:false};var savedhandlers={};var mouseUpHandler=null;function onMouseMove(e){if(selection.active){updateSelection(e);plot.getPlaceholder().trigger("plotselecting",[getSelection()])}}function onMouseDown(e){if(e.which!=1)return;document.body.focus();if(document.onselectstart!==undefined&&savedhandlers.onselectstart==null){savedhandlers.onselectstart=document.onselectstart;document.onselectstart=function(){return false}}if(document.ondrag!==undefined&&savedhandlers.ondrag==null){savedhandlers.ondrag=document.ondrag;document.ondrag=function(){return false}}setSelectionPos(selection.first,e);selection.active=true;mouseUpHandler=function(e){onMouseUp(e)};$(document).one("mouseup",mouseUpHandler)}function onMouseUp(e){mouseUpHandler=null;if(document.onselectstart!==undefined)document.onselectstart=savedhandlers.onselectstart;if(document.ondrag!==undefined)document.ondrag=savedhandlers.ondrag;selection.active=false;updateSelection(e);if(selectionIsSane())triggerSelectedEvent();else{plot.getPlaceholder().trigger("plotunselected",[]);plot.getPlaceholder().trigger("plotselecting",[null])}return false}function getSelection(){if(!selectionIsSane())return null;if(!selection.show)return null;var r={},c1=selection.first,c2=selection.second;$.each(plot.getAxes(),function(name,axis){if(axis.used){var p1=axis.c2p(c1[axis.direction]),p2=axis.c2p(c2[axis.direction]);r[name]={from:Math.min(p1,p2),to:Math.max(p1,p2)}}});return r}function triggerSelectedEvent(){var r=getSelection();plot.getPlaceholder().trigger("plotselected",[r]);if(r.xaxis&&r.yaxis)plot.getPlaceholder().trigger("selected",[{x1:r.xaxis.from,y1:r.yaxis.from,x2:r.xaxis.to,y2:r.yaxis.to}])}function clamp(min,value,max){return valuemax?max:value}function setSelectionPos(pos,e){var o=plot.getOptions();var offset=plot.getPlaceholder().offset();var plotOffset=plot.getPlotOffset();pos.x=clamp(0,e.pageX-offset.left-plotOffset.left,plot.width());pos.y=clamp(0,e.pageY-offset.top-plotOffset.top,plot.height());if(o.selection.mode=="y")pos.x=pos==selection.first?0:plot.width();if(o.selection.mode=="x")pos.y=pos==selection.first?0:plot.height()}function updateSelection(pos){if(pos.pageX==null)return;setSelectionPos(selection.second,pos);if(selectionIsSane()){selection.show=true;plot.triggerRedrawOverlay()}else clearSelection(true)}function clearSelection(preventEvent){if(selection.show){selection.show=false;plot.triggerRedrawOverlay();if(!preventEvent)plot.getPlaceholder().trigger("plotunselected",[])}}function extractRange(ranges,coord){var axis,from,to,key,axes=plot.getAxes();for(var k in axes){axis=axes[k];if(axis.direction==coord){key=coord+axis.n+"axis";if(!ranges[key]&&axis.n==1)key=coord+"axis";if(ranges[key]){from=ranges[key].from;to=ranges[key].to;break}}}if(!ranges[key]){axis=coord=="x"?plot.getXAxes()[0]:plot.getYAxes()[0];from=ranges[coord+"1"];to=ranges[coord+"2"]}if(from!=null&&to!=null&&from>to){var tmp=from;from=to;to=tmp}return{from:from,to:to,axis:axis}}function setSelection(ranges,preventEvent){var axis,range,o=plot.getOptions();if(o.selection.mode=="y"){selection.first.x=0;selection.second.x=plot.width()}else{range=extractRange(ranges,"x");selection.first.x=range.axis.p2c(range.from);selection.second.x=range.axis.p2c(range.to)}if(o.selection.mode=="x"){selection.first.y=0;selection.second.y=plot.height()}else{range=extractRange(ranges,"y");selection.first.y=range.axis.p2c(range.from);selection.second.y=range.axis.p2c(range.to)}selection.show=true;plot.triggerRedrawOverlay();if(!preventEvent&&selectionIsSane())triggerSelectedEvent()}function selectionIsSane(){var minSize=plot.getOptions().selection.minSize;return Math.abs(selection.second.x-selection.first.x)>=minSize&&Math.abs(selection.second.y-selection.first.y)>=minSize}plot.clearSelection=clearSelection;plot.setSelection=setSelection;plot.getSelection=getSelection;plot.hooks.bindEvents.push(function(plot,eventHolder){var o=plot.getOptions();if(o.selection.mode!=null){eventHolder.mousemove(onMouseMove);eventHolder.mousedown(onMouseDown)}});plot.hooks.drawOverlay.push(function(plot,ctx){if(selection.show&&selectionIsSane()){var plotOffset=plot.getPlotOffset();var o=plot.getOptions();ctx.save();ctx.translate(plotOffset.left,plotOffset.top);var c=$.color.parse(o.selection.color);ctx.strokeStyle=c.scale("a",.8).toString();ctx.lineWidth=1;ctx.lineJoin=o.selection.shape;ctx.fillStyle=c.scale("a",.4).toString();var x=Math.min(selection.first.x,selection.second.x)+.5,y=Math.min(selection.first.y,selection.second.y)+.5,w=Math.abs(selection.second.x-selection.first.x)-1,h=Math.abs(selection.second.y-selection.first.y)-1;ctx.fillRect(x,y,w,h);ctx.strokeRect(x,y,w,h);ctx.restore()}});plot.hooks.shutdown.push(function(plot,eventHolder){eventHolder.unbind("mousemove",onMouseMove);eventHolder.unbind("mousedown",onMouseDown);if(mouseUpHandler)$(document).unbind("mouseup",mouseUpHandler)})}$.plot.plugins.push({init:init,options:{selection:{mode:null,color:"#e8cfac",shape:"round",minSize:5}},name:"selection",version:"1.1"})})(jQuery); -------------------------------------------------------------------------------- /public/js/flot/jquery.flot.stack.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for stacking data sets rather than overlyaing them. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | The plugin assumes the data is sorted on x (or y if stacking horizontally). 7 | For line charts, it is assumed that if a line has an undefined gap (from a 8 | null point), then the line above it should have the same gap - insert zeros 9 | instead of "null" if you want another behaviour. This also holds for the start 10 | and end of the chart. Note that stacking a mix of positive and negative values 11 | in most instances doesn't make sense (so it looks weird). 12 | 13 | Two or more series are stacked when their "stack" attribute is set to the same 14 | key (which can be any number or string or just "true"). To specify the default 15 | stack, you can set the stack option like this: 16 | 17 | series: { 18 | stack: null/false, true, or a key (number/string) 19 | } 20 | 21 | You can also specify it for a single series, like this: 22 | 23 | $.plot( $("#placeholder"), [{ 24 | data: [ ... ], 25 | stack: true 26 | }]) 27 | 28 | The stacking order is determined by the order of the data series in the array 29 | (later series end up on top of the previous). 30 | 31 | Internally, the plugin modifies the datapoints in each series, adding an 32 | offset to the y value. For line series, extra data points are inserted through 33 | interpolation. If there's a second y value, it's also adjusted (e.g for bar 34 | charts or filled areas). 35 | 36 | */ 37 | 38 | (function ($) { 39 | var options = { 40 | series: { stack: null } // or number/string 41 | }; 42 | 43 | function init(plot) { 44 | function findMatchingSeries(s, allseries) { 45 | var res = null; 46 | for (var i = 0; i < allseries.length; ++i) { 47 | if (s == allseries[i]) 48 | break; 49 | 50 | if (allseries[i].stack == s.stack) 51 | res = allseries[i]; 52 | } 53 | 54 | return res; 55 | } 56 | 57 | function stackData(plot, s, datapoints) { 58 | if (s.stack == null || s.stack === false) 59 | return; 60 | 61 | var other = findMatchingSeries(s, plot.getData()); 62 | if (!other) 63 | return; 64 | 65 | var ps = datapoints.pointsize, 66 | points = datapoints.points, 67 | otherps = other.datapoints.pointsize, 68 | otherpoints = other.datapoints.points, 69 | newpoints = [], 70 | px, py, intery, qx, qy, bottom, 71 | withlines = s.lines.show, 72 | horizontal = s.bars.horizontal, 73 | withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), 74 | withsteps = withlines && s.lines.steps, 75 | fromgap = true, 76 | keyOffset = horizontal ? 1 : 0, 77 | accumulateOffset = horizontal ? 0 : 1, 78 | i = 0, j = 0, l, m; 79 | 80 | while (true) { 81 | if (i >= points.length) 82 | break; 83 | 84 | l = newpoints.length; 85 | 86 | if (points[i] == null) { 87 | // copy gaps 88 | for (m = 0; m < ps; ++m) 89 | newpoints.push(points[i + m]); 90 | i += ps; 91 | } 92 | else if (j >= otherpoints.length) { 93 | // for lines, we can't use the rest of the points 94 | if (!withlines) { 95 | for (m = 0; m < ps; ++m) 96 | newpoints.push(points[i + m]); 97 | } 98 | i += ps; 99 | } 100 | else if (otherpoints[j] == null) { 101 | // oops, got a gap 102 | for (m = 0; m < ps; ++m) 103 | newpoints.push(null); 104 | fromgap = true; 105 | j += otherps; 106 | } 107 | else { 108 | // cases where we actually got two points 109 | px = points[i + keyOffset]; 110 | py = points[i + accumulateOffset]; 111 | qx = otherpoints[j + keyOffset]; 112 | qy = otherpoints[j + accumulateOffset]; 113 | bottom = 0; 114 | 115 | if (px == qx) { 116 | for (m = 0; m < ps; ++m) 117 | newpoints.push(points[i + m]); 118 | 119 | newpoints[l + accumulateOffset] += qy; 120 | bottom = qy; 121 | 122 | i += ps; 123 | j += otherps; 124 | } 125 | else if (px > qx) { 126 | // we got past point below, might need to 127 | // insert interpolated extra point 128 | if (withlines && i > 0 && points[i - ps] != null) { 129 | intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); 130 | newpoints.push(qx); 131 | newpoints.push(intery + qy); 132 | for (m = 2; m < ps; ++m) 133 | newpoints.push(points[i + m]); 134 | bottom = qy; 135 | } 136 | 137 | j += otherps; 138 | } 139 | else { // px < qx 140 | if (fromgap && withlines) { 141 | // if we come from a gap, we just skip this point 142 | i += ps; 143 | continue; 144 | } 145 | 146 | for (m = 0; m < ps; ++m) 147 | newpoints.push(points[i + m]); 148 | 149 | // we might be able to interpolate a point below, 150 | // this can give us a better y 151 | if (withlines && j > 0 && otherpoints[j - otherps] != null) 152 | bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); 153 | 154 | newpoints[l + accumulateOffset] += bottom; 155 | 156 | i += ps; 157 | } 158 | 159 | fromgap = false; 160 | 161 | if (l != newpoints.length && withbottom) 162 | newpoints[l + 2] += bottom; 163 | } 164 | 165 | // maintain the line steps invariant 166 | if (withsteps && l != newpoints.length && l > 0 167 | && newpoints[l] != null 168 | && newpoints[l] != newpoints[l - ps] 169 | && newpoints[l + 1] != newpoints[l - ps + 1]) { 170 | for (m = 0; m < ps; ++m) 171 | newpoints[l + ps + m] = newpoints[l + m]; 172 | newpoints[l + 1] = newpoints[l - ps + 1]; 173 | } 174 | } 175 | 176 | datapoints.points = newpoints; 177 | } 178 | 179 | plot.hooks.processDatapoints.push(stackData); 180 | } 181 | 182 | $.plot.plugins.push({ 183 | init: init, 184 | options: options, 185 | name: 'stack', 186 | version: '1.2' 187 | }); 188 | })(jQuery); 189 | -------------------------------------------------------------------------------- /public/js/flot/jquery.flot.stack.min.js: -------------------------------------------------------------------------------- 1 | /* Javascript plotting library for jQuery, version 0.8.3. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | */ 7 | (function($){var options={series:{stack:null}};function init(plot){function findMatchingSeries(s,allseries){var res=null;for(var i=0;i2&&(horizontal?datapoints.format[2].x:datapoints.format[2].y),withsteps=withlines&&s.lines.steps,fromgap=true,keyOffset=horizontal?1:0,accumulateOffset=horizontal?0:1,i=0,j=0,l,m;while(true){if(i>=points.length)break;l=newpoints.length;if(points[i]==null){for(m=0;m=otherpoints.length){if(!withlines){for(m=0;mqx){if(withlines&&i>0&&points[i-ps]!=null){intery=py+(points[i-ps+accumulateOffset]-py)*(qx-px)/(points[i-ps+keyOffset]-px);newpoints.push(qx);newpoints.push(intery+qy);for(m=2;m0&&otherpoints[j-otherps]!=null)bottom=qy+(otherpoints[j-otherps+accumulateOffset]-qy)*(px-qx)/(otherpoints[j-otherps+keyOffset]-qx);newpoints[l+accumulateOffset]+=bottom;i+=ps}fromgap=false;if(l!=newpoints.length&&withbottom)newpoints[l+2]+=bottom}if(withsteps&&l!=newpoints.length&&l>0&&newpoints[l]!=null&&newpoints[l]!=newpoints[l-ps]&&newpoints[l+1]!=newpoints[l-ps+1]){for(m=0;m s = r * sqrt(pi)/2 24 | var size = radius * Math.sqrt(Math.PI) / 2; 25 | ctx.rect(x - size, y - size, size + size, size + size); 26 | }, 27 | diamond: function (ctx, x, y, radius, shadow) { 28 | // pi * r^2 = 2s^2 => s = r * sqrt(pi/2) 29 | var size = radius * Math.sqrt(Math.PI / 2); 30 | ctx.moveTo(x - size, y); 31 | ctx.lineTo(x, y - size); 32 | ctx.lineTo(x + size, y); 33 | ctx.lineTo(x, y + size); 34 | ctx.lineTo(x - size, y); 35 | }, 36 | triangle: function (ctx, x, y, radius, shadow) { 37 | // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3)) 38 | var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); 39 | var height = size * Math.sin(Math.PI / 3); 40 | ctx.moveTo(x - size/2, y + height/2); 41 | ctx.lineTo(x + size/2, y + height/2); 42 | if (!shadow) { 43 | ctx.lineTo(x, y - height/2); 44 | ctx.lineTo(x - size/2, y + height/2); 45 | } 46 | }, 47 | cross: function (ctx, x, y, radius, shadow) { 48 | // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 49 | var size = radius * Math.sqrt(Math.PI) / 2; 50 | ctx.moveTo(x - size, y - size); 51 | ctx.lineTo(x + size, y + size); 52 | ctx.moveTo(x - size, y + size); 53 | ctx.lineTo(x + size, y - size); 54 | } 55 | }; 56 | 57 | var s = series.points.symbol; 58 | if (handlers[s]) 59 | series.points.symbol = handlers[s]; 60 | } 61 | 62 | function init(plot) { 63 | plot.hooks.processDatapoints.push(processRawData); 64 | } 65 | 66 | $.plot.plugins.push({ 67 | init: init, 68 | name: 'symbols', 69 | version: '1.0' 70 | }); 71 | })(jQuery); 72 | -------------------------------------------------------------------------------- /public/js/flot/jquery.flot.symbol.min.js: -------------------------------------------------------------------------------- 1 | /* Javascript plotting library for jQuery, version 0.8.3. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | */ 7 | (function($){function processRawData(plot,series,datapoints){var handlers={square:function(ctx,x,y,radius,shadow){var size=radius*Math.sqrt(Math.PI)/2;ctx.rect(x-size,y-size,size+size,size+size)},diamond:function(ctx,x,y,radius,shadow){var size=radius*Math.sqrt(Math.PI/2);ctx.moveTo(x-size,y);ctx.lineTo(x,y-size);ctx.lineTo(x+size,y);ctx.lineTo(x,y+size);ctx.lineTo(x-size,y)},triangle:function(ctx,x,y,radius,shadow){var size=radius*Math.sqrt(2*Math.PI/Math.sin(Math.PI/3));var height=size*Math.sin(Math.PI/3);ctx.moveTo(x-size/2,y+height/2);ctx.lineTo(x+size/2,y+height/2);if(!shadow){ctx.lineTo(x,y-height/2);ctx.lineTo(x-size/2,y+height/2)}},cross:function(ctx,x,y,radius,shadow){var size=radius*Math.sqrt(Math.PI)/2;ctx.moveTo(x-size,y-size);ctx.lineTo(x+size,y+size);ctx.moveTo(x-size,y+size);ctx.lineTo(x+size,y-size)}};var s=series.points.symbol;if(handlers[s])series.points.symbol=handlers[s]}function init(plot){plot.hooks.processDatapoints.push(processRawData)}$.plot.plugins.push({init:init,name:"symbols",version:"1.0"})})(jQuery); -------------------------------------------------------------------------------- /public/js/flot/jquery.flot.threshold.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for thresholding data. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | The plugin supports these options: 7 | 8 | series: { 9 | threshold: { 10 | below: number 11 | color: colorspec 12 | } 13 | } 14 | 15 | It can also be applied to a single series, like this: 16 | 17 | $.plot( $("#placeholder"), [{ 18 | data: [ ... ], 19 | threshold: { ... } 20 | }]) 21 | 22 | An array can be passed for multiple thresholding, like this: 23 | 24 | threshold: [{ 25 | below: number1 26 | color: color1 27 | },{ 28 | below: number2 29 | color: color2 30 | }] 31 | 32 | These multiple threshold objects can be passed in any order since they are 33 | sorted by the processing function. 34 | 35 | The data points below "below" are drawn with the specified color. This makes 36 | it easy to mark points below 0, e.g. for budget data. 37 | 38 | Internally, the plugin works by splitting the data into two series, above and 39 | below the threshold. The extra series below the threshold will have its label 40 | cleared and the special "originSeries" attribute set to the original series. 41 | You may need to check for this in hover events. 42 | 43 | */ 44 | 45 | (function ($) { 46 | var options = { 47 | series: { threshold: null } // or { below: number, color: color spec} 48 | }; 49 | 50 | function init(plot) { 51 | function thresholdData(plot, s, datapoints, below, color) { 52 | var ps = datapoints.pointsize, i, x, y, p, prevp, 53 | thresholded = $.extend({}, s); // note: shallow copy 54 | 55 | thresholded.datapoints = { points: [], pointsize: ps, format: datapoints.format }; 56 | thresholded.label = null; 57 | thresholded.color = color; 58 | thresholded.threshold = null; 59 | thresholded.originSeries = s; 60 | thresholded.data = []; 61 | 62 | var origpoints = datapoints.points, 63 | addCrossingPoints = s.lines.show; 64 | 65 | var threspoints = []; 66 | var newpoints = []; 67 | var m; 68 | 69 | for (i = 0; i < origpoints.length; i += ps) { 70 | x = origpoints[i]; 71 | y = origpoints[i + 1]; 72 | 73 | prevp = p; 74 | if (y < below) 75 | p = threspoints; 76 | else 77 | p = newpoints; 78 | 79 | if (addCrossingPoints && prevp != p && x != null 80 | && i > 0 && origpoints[i - ps] != null) { 81 | var interx = x + (below - y) * (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]); 82 | prevp.push(interx); 83 | prevp.push(below); 84 | for (m = 2; m < ps; ++m) 85 | prevp.push(origpoints[i + m]); 86 | 87 | p.push(null); // start new segment 88 | p.push(null); 89 | for (m = 2; m < ps; ++m) 90 | p.push(origpoints[i + m]); 91 | p.push(interx); 92 | p.push(below); 93 | for (m = 2; m < ps; ++m) 94 | p.push(origpoints[i + m]); 95 | } 96 | 97 | p.push(x); 98 | p.push(y); 99 | for (m = 2; m < ps; ++m) 100 | p.push(origpoints[i + m]); 101 | } 102 | 103 | datapoints.points = newpoints; 104 | thresholded.datapoints.points = threspoints; 105 | 106 | if (thresholded.datapoints.points.length > 0) { 107 | var origIndex = $.inArray(s, plot.getData()); 108 | // Insert newly-generated series right after original one (to prevent it from becoming top-most) 109 | plot.getData().splice(origIndex + 1, 0, thresholded); 110 | } 111 | 112 | // FIXME: there are probably some edge cases left in bars 113 | } 114 | 115 | function processThresholds(plot, s, datapoints) { 116 | if (!s.threshold) 117 | return; 118 | 119 | if (s.threshold instanceof Array) { 120 | s.threshold.sort(function(a, b) { 121 | return a.below - b.below; 122 | }); 123 | 124 | $(s.threshold).each(function(i, th) { 125 | thresholdData(plot, s, datapoints, th.below, th.color); 126 | }); 127 | } 128 | else { 129 | thresholdData(plot, s, datapoints, s.threshold.below, s.threshold.color); 130 | } 131 | } 132 | 133 | plot.hooks.processDatapoints.push(processThresholds); 134 | } 135 | 136 | $.plot.plugins.push({ 137 | init: init, 138 | options: options, 139 | name: 'threshold', 140 | version: '1.2' 141 | }); 142 | })(jQuery); 143 | -------------------------------------------------------------------------------- /public/js/flot/jquery.flot.threshold.min.js: -------------------------------------------------------------------------------- 1 | /* Javascript plotting library for jQuery, version 0.8.3. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | */ 7 | (function($){var options={series:{threshold:null}};function init(plot){function thresholdData(plot,s,datapoints,below,color){var ps=datapoints.pointsize,i,x,y,p,prevp,thresholded=$.extend({},s);thresholded.datapoints={points:[],pointsize:ps,format:datapoints.format};thresholded.label=null;thresholded.color=color;thresholded.threshold=null;thresholded.originSeries=s;thresholded.data=[];var origpoints=datapoints.points,addCrossingPoints=s.lines.show;var threspoints=[];var newpoints=[];var m;for(i=0;i0&&origpoints[i-ps]!=null){var interx=x+(below-y)*(x-origpoints[i-ps])/(y-origpoints[i-ps+1]);prevp.push(interx);prevp.push(below);for(m=2;m0){var origIndex=$.inArray(s,plot.getData());plot.getData().splice(origIndex+1,0,thresholded)}}function processThresholds(plot,s,datapoints){if(!s.threshold)return;if(s.threshold instanceof Array){s.threshold.sort(function(a,b){return a.below-b.below});$(s.threshold).each(function(i,th){thresholdData(plot,s,datapoints,th.below,th.color)})}else{thresholdData(plot,s,datapoints,s.threshold.below,s.threshold.color)}}plot.hooks.processDatapoints.push(processThresholds)}$.plot.plugins.push({init:init,options:options,name:"threshold",version:"1.2"})})(jQuery); -------------------------------------------------------------------------------- /public/js/flot/jquery.flot.time.min.js: -------------------------------------------------------------------------------- 1 | /* Javascript plotting library for jQuery, version 0.8.3. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | */ 7 | (function($){var options={xaxis:{timezone:null,timeformat:null,twelveHourClock:false,monthNames:null}};function floorInBase(n,base){return base*Math.floor(n/base)}function formatDate(d,fmt,monthNames,dayNames){if(typeof d.strftime=="function"){return d.strftime(fmt)}var leftPad=function(n,pad){n=""+n;pad=""+(pad==null?"0":pad);return n.length==1?pad+n:n};var r=[];var escape=false;var hours=d.getHours();var isAM=hours<12;if(monthNames==null){monthNames=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}if(dayNames==null){dayNames=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]}var hours12;if(hours>12){hours12=hours-12}else if(hours==0){hours12=12}else{hours12=hours}for(var i=0;i=minSize){break}}var size=spec[i][0];var unit=spec[i][1];if(unit=="year"){if(opts.minTickSize!=null&&opts.minTickSize[1]=="year"){size=Math.floor(opts.minTickSize[0])}else{var magn=Math.pow(10,Math.floor(Math.log(axis.delta/timeUnitSize.year)/Math.LN10));var norm=axis.delta/timeUnitSize.year/magn;if(norm<1.5){size=1}else if(norm<3){size=2}else if(norm<7.5){size=5}else{size=10}size*=magn}if(size<1){size=1}}axis.tickSize=opts.tickSize||[size,unit];var tickSize=axis.tickSize[0];unit=axis.tickSize[1];var step=tickSize*timeUnitSize[unit];if(unit=="second"){d.setSeconds(floorInBase(d.getSeconds(),tickSize))}else if(unit=="minute"){d.setMinutes(floorInBase(d.getMinutes(),tickSize))}else if(unit=="hour"){d.setHours(floorInBase(d.getHours(),tickSize))}else if(unit=="month"){d.setMonth(floorInBase(d.getMonth(),tickSize))}else if(unit=="quarter"){d.setMonth(3*floorInBase(d.getMonth()/3,tickSize))}else if(unit=="year"){d.setFullYear(floorInBase(d.getFullYear(),tickSize))}d.setMilliseconds(0);if(step>=timeUnitSize.minute){d.setSeconds(0)}if(step>=timeUnitSize.hour){d.setMinutes(0)}if(step>=timeUnitSize.day){d.setHours(0)}if(step>=timeUnitSize.day*4){d.setDate(1)}if(step>=timeUnitSize.month*2){d.setMonth(floorInBase(d.getMonth(),3))}if(step>=timeUnitSize.quarter*2){d.setMonth(floorInBase(d.getMonth(),6))}if(step>=timeUnitSize.year){d.setMonth(0)}var carry=0;var v=Number.NaN;var prev;do{prev=v;v=d.getTime();ticks.push(v);if(unit=="month"||unit=="quarter"){if(tickSize<1){d.setDate(1);var start=d.getTime();d.setMonth(d.getMonth()+(unit=="quarter"?3:1));var end=d.getTime();d.setTime(v+carry*timeUnitSize.hour+(end-start)*tickSize);carry=d.getHours();d.setHours(0)}else{d.setMonth(d.getMonth()+tickSize*(unit=="quarter"?3:1))}}else if(unit=="year"){d.setFullYear(d.getFullYear()+tickSize)}else{d.setTime(v+step)}}while(v