├── .gitignore ├── package.json ├── monitors ├── memory.js ├── uptime.js ├── temps.js ├── cpu.js ├── processes.js └── network.js ├── public ├── css │ ├── darkmode.css │ ├── light.css │ └── style.css ├── js │ ├── widgets │ │ ├── basewidget.js │ │ ├── memory.js │ │ ├── processcpu.js │ │ ├── processmemory.js │ │ ├── cpuusage.js │ │ ├── uptime.js │ │ ├── temps.js │ │ ├── freemem.js │ │ ├── cpuchart.js │ │ ├── network.js │ │ ├── basepiewidget.js │ │ └── basechartwidget.js │ ├── theme.js │ ├── script.js │ └── lib │ │ ├── moment.min.js │ │ └── Chart.min.js └── index.html ├── app.js ├── README.md ├── LICENSE.md └── monitor.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | up.sh -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mintr", 3 | "author": "Kevin Bedi", 4 | "version": "0.0.12", 5 | "description": "A simple unix monitor", 6 | "main": "app.js", 7 | "bin": "./app.js", 8 | "license": "MIT", 9 | "dependencies": { 10 | "async": "^0.9.0", 11 | "express": "^4.19.2", 12 | "socket.io": "^4.6.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /monitors/memory.js: -------------------------------------------------------------------------------- 1 | var os = require('os'); 2 | 3 | var Memory = {}; 4 | 5 | Memory.monitor = function(history, callback) { 6 | callback({ 7 | total: os.totalmem(), 8 | free: os.freemem(), 9 | }); 10 | }; 11 | 12 | Memory.history = true; 13 | Memory.maxHistory = 3600; 14 | Memory.frequency = 20; 15 | 16 | module.exports = Memory; 17 | -------------------------------------------------------------------------------- /public/css/darkmode.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #060606; 3 | } 4 | 5 | .theme-button::after { 6 | content: "🌙"; 7 | } 8 | 9 | .header { 10 | background: #000; 11 | box-shadow: 0 0 5px #444; 12 | } 13 | 14 | .widget { 15 | background: #000; 16 | border: 1px solid #0f0f0f; 17 | box-shadow: 0 0 5px #111; 18 | } 19 | 20 | .widget-title { 21 | background: #111; 22 | color: #aaa; 23 | border-bottom: 1px solid #444; 24 | } 25 | 26 | .tooltip { 27 | background: #000; 28 | color: #fff; 29 | box-shadow: 0 2px 6px -4px #111; 30 | } -------------------------------------------------------------------------------- /public/css/light.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #fafafa; 3 | } 4 | 5 | .theme-button::after { 6 | content: "🌞"; 7 | } 8 | 9 | .header { 10 | background: white; 11 | box-shadow: 0 0 5px #888; 12 | } 13 | 14 | .widget { 15 | background: #fff; 16 | border: 1px solid #f0f0f0; 17 | box-shadow: 0 0 5px #ccc; 18 | } 19 | 20 | .widget-title { 21 | background: #f5f5f5; 22 | color: #aaa; 23 | border-bottom: 1px solid #eee; 24 | } 25 | 26 | .tooltip { 27 | background: #f9f9f9; 28 | color: #000; 29 | box-shadow: 0 2px 6px -4px #444; 30 | } -------------------------------------------------------------------------------- /monitors/uptime.js: -------------------------------------------------------------------------------- 1 | var os = require('os'); 2 | var exec = require('child_process').exec; 3 | 4 | var Uptime = {}; 5 | 6 | Uptime.monitor = function(history, callback) { 7 | exec('ps -eo lstart,args | grep /sbin/init', function(error, result) { 8 | var processTextIndex = result.indexOf('/sbin/init'); 9 | var dateText = result.substring(0, processTextIndex); 10 | var uptime = Date.now() - new Date(dateText).getTime(); 11 | 12 | callback(uptime); 13 | }); 14 | }; 15 | 16 | Uptime.history = false; 17 | Uptime.frequency = 1; 18 | 19 | module.exports = Uptime; 20 | -------------------------------------------------------------------------------- /public/js/widgets/basewidget.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var BaseWidget = function(widget) { 3 | this.div = document.createElement('div'); 4 | }; 5 | 6 | BaseWidget.prototype.getTitle = function() { 7 | return "Base Widget"; 8 | }; 9 | 10 | BaseWidget.prototype.getWidthClass = function() { 11 | return "one"; 12 | }; 13 | 14 | BaseWidget.prototype.getElement = function() { 15 | return this.div; 16 | }; 17 | 18 | BaseWidget.prototype.initialize = function(history) { 19 | }; 20 | 21 | BaseWidget.prototype.addData = function(data) { 22 | }; 23 | 24 | window.BaseWidget = BaseWidget; 25 | })(); 26 | -------------------------------------------------------------------------------- /public/js/theme.js: -------------------------------------------------------------------------------- 1 | const themes = ["light", "darkmode"]; 2 | 3 | $(function () { 4 | let currentTheme = window.localStorage.getItem("theme") ?? themes[0]; 5 | 6 | const setTheme = (theme) => { 7 | currentTheme = theme; 8 | window.localStorage.setItem("theme", currentTheme); 9 | 10 | document.getElementById("theme").href = "./css/" + currentTheme + ".css"; 11 | }; 12 | 13 | setTheme(currentTheme); 14 | 15 | $(".theme-button").on("click", () => { 16 | const curThemeIdx = themes.findIndex((x) => x === currentTheme); 17 | const newThemeIdx = (curThemeIdx + 1) % themes.length; 18 | 19 | setTheme(themes[newThemeIdx]); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var express = require('express'); 4 | var app = express(); 5 | 6 | var server = require('http').Server(app); 7 | var io = require('socket.io')(server); 8 | 9 | var monitor = require('./monitor'); 10 | monitor.on('data', function(data) { 11 | io.emit('data', data); 12 | }); 13 | 14 | app.use(express.static(__dirname + '/public')); 15 | 16 | var port = parseInt(process.argv[2]) || 3000; 17 | server.listen(port); 18 | console.log("Mintr is listening on port " + port); 19 | 20 | var sockets = {}; 21 | 22 | io.on('connection', function (socket) { 23 | sockets[socket.id] = socket; 24 | 25 | var history = monitor.getHistory(); 26 | socket.emit('history', history); 27 | 28 | socket.on('disconnect', function() { 29 | delete sockets[socket.id]; 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /monitors/temps.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | var Temps = {}; 4 | 5 | Temps.monitor = function (history, callback) { 6 | if (!fs.existsSync('/sys/class/thermal')) { 7 | callback({}); 8 | return; 9 | } 10 | 11 | const tempSensorDirs = fs.readdirSync('/sys/class/thermal') 12 | .filter(x => x.startsWith('thermal')); 13 | 14 | const temps = tempSensorDirs.map(dir => { 15 | const dirType = fs.readFileSync('/sys/class/thermal/' + dir + '/type', { encoding: 'utf8' }); 16 | const dirTemp = fs.readFileSync('/sys/class/thermal/' + dir + '/temp', { encoding: 'utf8' }); 17 | 18 | return { type: dirType.trim(), temp: parseInt(dirTemp.trim()) }; 19 | }); 20 | 21 | callback(temps); 22 | }; 23 | 24 | Temps.history = false; 25 | Temps.frequency = 20; 26 | 27 | module.exports = Temps; 28 | -------------------------------------------------------------------------------- /monitors/cpu.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | var os = require('os'); 3 | 4 | var CPU = {}; 5 | 6 | CPU.monitor = function(history, callback) { 7 | exec('ps aux', function(error, result) { 8 | var cpu = 0; 9 | 10 | var lines = result.split('\n'); 11 | lines.splice(0, 1); 12 | 13 | for (var x = 0; x < lines.length; x++) { 14 | if (!lines[x].trim()) { 15 | continue; 16 | } 17 | cpu += parseFloat(lines[x].split(/\s+/)[2]); 18 | } 19 | 20 | // Also record a normalized CPU usage per thread 21 | var normalized = cpu / os.cpus().length; 22 | 23 | callback({ 24 | cpu: cpu, 25 | normalized: normalized, 26 | timestamp: Date.now(), 27 | }); 28 | }); 29 | }; 30 | 31 | CPU.history = true; 32 | CPU.maxHistory = 3600; 33 | CPU.frequency = 20; 34 | 35 | module.exports = CPU; 36 | -------------------------------------------------------------------------------- /public/js/widgets/memory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var MemoryWidget = function(widget) { 3 | BaseWidget.call(this, widget); 4 | }; 5 | MemoryWidget.prototype = Object.create(BaseWidget.prototype); 6 | 7 | MemoryWidget.prototype.getTitle = function() { 8 | return "Used Memory"; 9 | }; 10 | 11 | MemoryWidget.prototype.getWidthClass = function() { 12 | return "one"; 13 | }; 14 | 15 | MemoryWidget.prototype.initialize = function(history) { 16 | this.addData({ 17 | memory: history.memory[history.memory.length - 1] 18 | }); 19 | }; 20 | 21 | MemoryWidget.prototype.addData = function(data) { 22 | if (!data.memory) { 23 | return; 24 | } 25 | 26 | var usedMemMB = 27 | Math.floor((data.memory.total - data.memory.free) / 1000000); 28 | var totalMemMB = Math.floor(data.memory.total / 1000000); 29 | 30 | this.div.innerText = 31 | "Used Memory: " + usedMemMB + "MB / " + totalMemMB + "MB"; 32 | }; 33 | 34 | window.MemoryWidget = MemoryWidget; 35 | })(); 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mintr 2 | 3 | ![Screenshot](http://i.imgur.com/7gYEhQ3.png) 4 | 5 | Mintr is a simple unix monitoring tool. It intentially avoids authentication, and thus does not monitor or display any private information, nor does it allow any actions to be taken from the web interface. 6 | 7 | It, quite simply, shows a few graphs that should help indicate the current status of your server, as well as the status over the past hour. 8 | 9 | Currently it shows 10 | 11 | * Memory usage 12 | * CPU Usage 13 | * Network activity 14 | * Process memory usage & CPU usage 15 | * Temperature Sensors 16 | 17 | # Installation & Usage 18 | 19 | ``` 20 | npm install -g mintr 21 | mintr 22 | ``` 23 | 24 | If you do not have nodejs/npm, you'll need to install that first. See https://nodejs.org/ for instructions on this. 25 | 26 | `mintr` takes an optional parameter which is the port it runs on, so you can run `mintr 1337` if you want to run it on port `1337`. The default port is `3000`. 27 | You can run `mintr` in a screen/tmux, add it to your system's startup, or anything else you please. 28 | Then go to `http://your-server.com:3000`, or replace `3000` with the correct port. 29 | -------------------------------------------------------------------------------- /public/js/widgets/processcpu.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var COLORS = ['#F44336', '#3F51B5', '#4CAF50', '#00BCD4', '#FF9800']; 3 | 4 | var ProcessCPUWidget = function(widget) { 5 | BasePieWidget.call(this, widget); 6 | }; 7 | ProcessCPUWidget.prototype = Object.create(BasePieWidget.prototype); 8 | 9 | ProcessCPUWidget.prototype.getTitle = function() { 10 | return "Process CPU Usage"; 11 | }; 12 | 13 | ProcessCPUWidget.prototype.getSuffix = function() { 14 | return "% CPU"; 15 | }; 16 | 17 | ProcessCPUWidget.prototype.getChartData = function(data) { 18 | if (!data.processes) { 19 | return false; 20 | } 21 | 22 | var processes = data.processes; 23 | 24 | processes.sort(function(a, b) { 25 | return b.cpu - a.cpu; 26 | }); 27 | 28 | processes = processes.slice(0, 5); 29 | 30 | var chartData = []; 31 | processes.forEach(function(process, index) { 32 | chartData.push({ 33 | value: process.cpu, 34 | color: COLORS[index], 35 | label: process.name, 36 | }); 37 | }); 38 | 39 | return chartData; 40 | }; 41 | 42 | window.ProcessCPUWidget = ProcessCPUWidget; 43 | })(); 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2015 Kevin Bedi 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /public/js/widgets/processmemory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var COLORS = ['#F44336', '#3F51B5', '#4CAF50', '#00BCD4', '#FF9800']; 3 | 4 | var ProcessMemoryWidget = function(widget) { 5 | BasePieWidget.call(this, widget); 6 | }; 7 | ProcessMemoryWidget.prototype = Object.create(BasePieWidget.prototype); 8 | 9 | ProcessMemoryWidget.prototype.getTitle = function() { 10 | return "Process Memory Usage"; 11 | }; 12 | 13 | ProcessMemoryWidget.prototype.getSuffix = function() { 14 | return "% Memory"; 15 | }; 16 | 17 | ProcessMemoryWidget.prototype.getChartData = function(data) { 18 | if (!data.processes) { 19 | return false; 20 | } 21 | 22 | var processes = data.processes; 23 | 24 | processes.sort(function(a, b) { 25 | return b.mem - a.mem; 26 | }); 27 | 28 | processes = processes.slice(0, 5); 29 | 30 | var chartData = []; 31 | processes.forEach(function(process, index) { 32 | chartData.push({ 33 | value: process.mem, 34 | color: COLORS[index], 35 | label: process.name, 36 | }); 37 | }); 38 | 39 | return chartData; 40 | }; 41 | 42 | window.ProcessMemoryWidget = ProcessMemoryWidget; 43 | })(); 44 | -------------------------------------------------------------------------------- /public/js/widgets/cpuusage.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var CPUUsageWidget = function(widget) { 3 | BaseWidget.call(this, widget); 4 | this.topDiv = document.createElement('div'); 5 | this.bottomDiv = document.createElement('div'); 6 | this.div.appendChild(this.topDiv); 7 | this.div.appendChild(this.bottomDiv); 8 | }; 9 | CPUUsageWidget.prototype = Object.create(BaseWidget.prototype); 10 | 11 | CPUUsageWidget.prototype.getTitle = function() { 12 | return "CPU Load"; 13 | }; 14 | 15 | CPUUsageWidget.prototype.getWidthClass = function() { 16 | return "one"; 17 | }; 18 | 19 | CPUUsageWidget.prototype.initialize = function(history) { 20 | this.addData(history); 21 | }; 22 | 23 | CPUUsageWidget.prototype.addData = function(data) { 24 | if (!data.cpu) { 25 | return false; 26 | } 27 | 28 | if (data.cpu.length) { 29 | var cpu = data.cpu[data.cpu.length - 1].cpu; 30 | var normalized = data.cpu[data.cpu.length - 1].normalized; 31 | } else { 32 | cpu = data.cpu.cpu; 33 | normalized = data.cpu.normalized; 34 | } 35 | 36 | this.topDiv.innerText = 37 | "CPU Load: " + cpu.toFixed(2) + "%"; 38 | this.bottomDiv.innerText = 39 | "Normalized: " + normalized.toFixed(2) + "% per thread"; 40 | }; 41 | 42 | window.CPUUsageWidget = CPUUsageWidget; 43 | })(); 44 | -------------------------------------------------------------------------------- /public/js/widgets/uptime.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var UptimeWidget = function(widget) { 3 | BaseWidget.call(this, widget); 4 | }; 5 | UptimeWidget.prototype = Object.create(BaseWidget.prototype); 6 | 7 | UptimeWidget.prototype.getTitle = function() { 8 | return "Uptime Information"; 9 | }; 10 | 11 | UptimeWidget.prototype.getWidthClass = function() { 12 | return "one"; 13 | }; 14 | 15 | UptimeWidget.prototype.getElement = function() { 16 | return this.div; 17 | }; 18 | 19 | UptimeWidget.prototype.initialize = function(history) { 20 | this.addData(history); 21 | }; 22 | 23 | UptimeWidget.prototype.addData = function(data) { 24 | var uptimeMilli = data.uptime; 25 | var duration = moment.duration(uptimeMilli); 26 | 27 | var years = duration.years(); 28 | var months = duration.months(); 29 | var days = duration.days(); 30 | var hours = duration.hours(); 31 | var minutes = duration.minutes(); 32 | var seconds = duration.seconds(); 33 | 34 | var times = [ 35 | [years, 'year'], 36 | [months, 'month'], 37 | [days, 'day'], 38 | [hours, 'hour'], 39 | [minutes, 'minute'], 40 | [seconds, 'second'], 41 | ]; 42 | 43 | var display = ""; 44 | for (var x = 0; x < times.length; x++) { 45 | var time = times[x]; 46 | if (time[0]) { 47 | display += time[0] + ' ' + time[1] + 48 | (time[0] !== 1 ? 's' : '') + 49 | (x !== times.length - 1 ? ', ' : ''); 50 | } 51 | } 52 | 53 | this.div.innerText = display; 54 | }; 55 | 56 | window.UptimeWidget = UptimeWidget; 57 | })(); 58 | -------------------------------------------------------------------------------- /public/js/widgets/temps.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var TempsWidget = function (widget) { 3 | BaseWidget.call(this, widget); 4 | 5 | this.divs = []; 6 | 7 | // this.topDiv = document.createElement('div'); 8 | // this.bottomDiv = document.createElement('div'); 9 | // this.div.appendChild(this.topDiv); 10 | // this.div.appendChild(this.bottomDiv); 11 | }; 12 | TempsWidget.prototype = Object.create(BaseWidget.prototype); 13 | 14 | TempsWidget.prototype.getTitle = function () { 15 | return "Temperature Sensors"; 16 | }; 17 | 18 | TempsWidget.prototype.getWidthClass = function () { 19 | return "one"; 20 | }; 21 | 22 | TempsWidget.prototype.initialize = function (history) { 23 | this.addData(history); 24 | }; 25 | 26 | TempsWidget.prototype.addData = function (data) { 27 | if (!data.temps) { 28 | return false; 29 | } 30 | 31 | // const latestData = data.temps[data.temps.length - 1]; 32 | 33 | console.log(data.temps); 34 | 35 | // TODO we might wanna support removing divs if the number of sensors goes 36 | // down somehow during runtime. 37 | if (this.divs.length < data.temps.length) { 38 | const numDivsToCreate = data.temps.length - this.divs.length; 39 | for (let x = 0; x < numDivsToCreate; x++) { 40 | const div = document.createElement('div'); 41 | this.div.appendChild(div); 42 | this.divs.push(div); 43 | } 44 | } 45 | 46 | data.temps.forEach((tempData, idx) => { 47 | const tempC = tempData.temp / 1000; 48 | this.divs[idx].innerText = `${tempData.type}: ${tempC}°C`; 49 | }); 50 | }; 51 | 52 | window.TempsWidget = TempsWidget; 53 | })(); 54 | -------------------------------------------------------------------------------- /monitors/processes.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | 3 | var MAX_NAME_LENGTH = 40; 4 | 5 | var Processes = {}; 6 | 7 | Processes.monitor = function(history, callback) { 8 | exec('ps aux', function(error, result) { 9 | var processes = []; 10 | 11 | var lines = result.split('\n'); 12 | lines.splice(0, 1); 13 | 14 | for (var x = 0; x < lines.length; x++) { 15 | var line = lines[x]; 16 | if (!line.trim()) { 17 | continue; 18 | } 19 | var process = {}; 20 | var words = line.split(/\s+/); 21 | process.user = words[0]; 22 | process.pid = parseInt(words[1]); 23 | process.cpu = parseFloat(words[2]); 24 | process.mem = parseFloat(words[3]); 25 | var name = words[10]; 26 | for (var y = 11; y < words.length; y++) { 27 | name += ' ' + words[y]; 28 | } 29 | process.name = name && name.substring(0, MAX_NAME_LENGTH); 30 | 31 | processes.push(process); 32 | } 33 | 34 | var topCPU = processes.sort(function(a, b) { 35 | return b.cpu - a.cpu; 36 | }).slice(0, 5); 37 | var topMem = processes.sort(function(a, b) { 38 | return b.mem - a.mem; 39 | }).slice(0, 5); 40 | 41 | var topProcesses = []; 42 | var topPIDs = {}; 43 | for (var x = 0; x < topCPU.length; x++) { 44 | topPIDs[topCPU[x].pid] = true; 45 | topProcesses.push(topCPU[x]); 46 | if (!topPIDs[topMem[x].pid]) { 47 | topPIDs[topMem[x].pid] = true; 48 | topProcesses.push(topMem[x]); 49 | } 50 | } 51 | 52 | callback(topProcesses); 53 | }); 54 | }; 55 | 56 | Processes.history = false; 57 | Processes.frequency = 20; 58 | 59 | module.exports = Processes; 60 | -------------------------------------------------------------------------------- /public/js/widgets/freemem.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var FreememWidget = function(widget) { 3 | BaseChartWidget.call(this, widget); 4 | }; 5 | FreememWidget.prototype = Object.create(BaseChartWidget.prototype); 6 | 7 | FreememWidget.prototype.getTitle = function() { 8 | return "Used Memory"; 9 | }; 10 | 11 | FreememWidget.prototype.getWidthClass = function() { 12 | return "three"; 13 | }; 14 | 15 | FreememWidget.prototype.getColors = function() { 16 | return ['#1E88E5']; 17 | }, 18 | 19 | FreememWidget.prototype.getFillColors = function() { 20 | return ['rgba(30,136,229,0.05)']; 21 | }; 22 | 23 | FreememWidget.prototype.getLabels = function() { 24 | return ['Used Memory']; 25 | }; 26 | 27 | FreememWidget.prototype.getSuffix = function() { 28 | return "MB"; 29 | }; 30 | 31 | FreememWidget.prototype.getPointsFromHistory = function(history) { 32 | var dataPoints = [] 33 | var labels = []; 34 | history.memory.forEach(function(data, index) { 35 | this.numPoints++; 36 | 37 | dataPoints.push(Math.floor((data.total - data.free) / 1000000)); 38 | labels.push( 39 | index % 5 === 0 40 | ? moment(data.timestamp).format('h:mm:ss') 41 | : '' 42 | ); 43 | }, this); 44 | 45 | return { 46 | values: [dataPoints], 47 | labels: labels, 48 | }; 49 | }; 50 | 51 | FreememWidget.prototype.getValuesFromData = function(data) { 52 | if (!data.memory) { 53 | return; 54 | } 55 | 56 | return [Math.floor((data.memory.total - data.memory.free) / 1000000)]; 57 | }; 58 | 59 | FreememWidget.prototype.getLabelFromData = function(data) { 60 | return moment(data.memory.timestamp).format('h:mm:ss'); 61 | }; 62 | 63 | window.FreememWidget = FreememWidget; 64 | })(); 65 | -------------------------------------------------------------------------------- /public/js/widgets/cpuchart.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var CPUChartWidget = function(widget) { 3 | BaseChartWidget.call(this, widget); 4 | }; 5 | CPUChartWidget.prototype = Object.create(BaseChartWidget.prototype); 6 | 7 | CPUChartWidget.prototype.getTitle = function() { 8 | return "CPU Usage"; 9 | }; 10 | 11 | CPUChartWidget.prototype.getWidthClass = function() { 12 | return "three"; 13 | }; 14 | 15 | CPUChartWidget.prototype.getColors = function() { 16 | return ['#4CAF50', '#1B5E20']; 17 | }, 18 | 19 | CPUChartWidget.prototype.getFillColors = function() { 20 | return ['rgba(76,175,80,0.05)', 'rgba(27,94,32, 0.05)']; 21 | }; 22 | 23 | CPUChartWidget.prototype.getLabels = function() { 24 | return ['CPU Usage', 'CPU Usage Per Thread']; 25 | }; 26 | 27 | CPUChartWidget.prototype.getSuffix = function() { 28 | return "%"; 29 | }; 30 | 31 | CPUChartWidget.prototype.getPointsFromHistory = function(history) { 32 | var dataPoints = []; 33 | var normalizedDataPoints = []; 34 | var labels = []; 35 | 36 | history.cpu.forEach(function(data, index) { 37 | this.numPoints++; 38 | 39 | dataPoints.push(data.cpu.toFixed(2)); 40 | normalizedDataPoints.push(data.normalized.toFixed(2)); 41 | labels.push( 42 | index % 5 === 0 43 | ? moment(data.timestamp).format('h:mm:ss') 44 | : '' 45 | ); 46 | }, this); 47 | 48 | return { 49 | values: [dataPoints, normalizedDataPoints], 50 | labels: labels, 51 | }; 52 | }; 53 | 54 | CPUChartWidget.prototype.getValuesFromData = function(data) { 55 | if (!data.cpu) { 56 | return; 57 | } 58 | 59 | return [data.cpu.cpu.toFixed(2), data.cpu.normalized.toFixed(2)]; 60 | }; 61 | 62 | CPUChartWidget.prototype.getLabelFromData = function(data) { 63 | return moment(data.cpu.timestamp).format('h:mm:ss'); 64 | }; 65 | 66 | window.CPUChartWidget = CPUChartWidget; 67 | })(); 68 | -------------------------------------------------------------------------------- /public/js/widgets/network.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var NetworkWidget = function(widget) { 3 | BaseChartWidget.call(this, widget); 4 | }; 5 | NetworkWidget.prototype = Object.create(BaseChartWidget.prototype); 6 | 7 | NetworkWidget.prototype.getTitle = function() { 8 | return "Network Activity"; 9 | }; 10 | 11 | NetworkWidget.prototype.getWidthClass = function() { 12 | return "three"; 13 | }; 14 | 15 | NetworkWidget.prototype.getColors = function() { 16 | return ['#F44336', '#B71C1C']; 17 | }, 18 | 19 | NetworkWidget.prototype.getFillColors = function() { 20 | return ['rgba(244,67,54,0.05)', 'rgba(183,28,28,0.05)']; 21 | }; 22 | 23 | NetworkWidget.prototype.getLabels = function() { 24 | return ['Incoming Speed', 'Outgoing Speed']; 25 | }; 26 | 27 | NetworkWidget.prototype.getSuffix = function() { 28 | return "kB/s"; 29 | }; 30 | 31 | NetworkWidget.prototype.getPointsFromHistory = function(history) { 32 | var inDataPoints = []; 33 | var outDataPoints = []; 34 | var labels = []; 35 | 36 | history.network.forEach(function(data, index) { 37 | this.numPoints++; 38 | 39 | inDataPoints.push(Math.floor(data.inSpeed / 1000)); 40 | outDataPoints.push(Math.floor(data.outSpeed / 1000)); 41 | labels.push( 42 | index % 5 === 0 43 | ? moment(data.timestamp).format('h:mm:ss') 44 | : '' 45 | ); 46 | }, this); 47 | 48 | return { 49 | values: [inDataPoints, outDataPoints], 50 | labels: labels, 51 | }; 52 | }; 53 | 54 | NetworkWidget.prototype.getValuesFromData = function(data) { 55 | if (!data.network) { 56 | return; 57 | } 58 | 59 | return [ 60 | Math.floor(data.network.inSpeed / 1000), 61 | Math.floor(data.network.outSpeed / 1000) 62 | ]; 63 | }; 64 | 65 | NetworkWidget.prototype.getLabelFromData = function(data) { 66 | return moment(data.network.timestamp).format('h:mm:ss'); 67 | }; 68 | 69 | window.NetworkWidget = NetworkWidget; 70 | })(); 71 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mintr 5 | 6 | 11 | 12 | 13 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 |
48 |
49 | Mintr 50 |
51 |
52 |
53 |
54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /monitors/network.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | var async = require('async'); 3 | var os = require('os'); 4 | 5 | var Memory = {}; 6 | 7 | Memory.monitor = function(history, callback) { 8 | exec('ip -s link', function(error, result) { 9 | if (error) { 10 | console.log( 11 | 'Mintr uses `ip` to get network information, which your ' + 12 | 'OS does not support.' 13 | ); 14 | console.log(error); 15 | callback({}); 16 | return; 17 | } 18 | 19 | var data = { 20 | in: 0, 21 | out: 0, 22 | }; 23 | 24 | var rx = result.match(/\d+:\s+\w+:\s+[\S\s]*?RX:\s+[\S\s]*?(\d+)/g); 25 | var tx = result.match(/\d+:\s+\w+:\s+[\S\s]*?TX:\s+[\S\s]*?(\d+)/g); 26 | 27 | for (var x = 0; x < rx.length; x++) { 28 | var localAmt = rx[x]; 29 | if (localAmt.indexOf('lo:') !== -1) { 30 | continue; 31 | } 32 | 33 | data.in += parseInt(localAmt.substring(localAmt.lastIndexOf('\n') + 1)); 34 | } 35 | 36 | for (var x = 0; x < rx.length; x++) { 37 | var localAmt = tx[x]; 38 | if (localAmt.indexOf('lo:') !== -1) { 39 | continue; 40 | } 41 | 42 | data.out += parseInt(localAmt.substring(localAmt.lastIndexOf('\n') + 1)); 43 | } 44 | 45 | data.timestamp = Date.now(); 46 | 47 | // Now calculate speeds 48 | if (history.length > 0) { 49 | var amountBack = 1; 50 | var index = history.length - amountBack; 51 | var lastTimestamp = history[index].timestamp; 52 | var lastOut = history[index].out; 53 | var lastIn = history[index].in; 54 | 55 | var elapsedTime = data.timestamp - lastTimestamp; 56 | var deltaOut = data.out - lastOut; 57 | var deltaIn = data.in - lastIn; 58 | 59 | data.outSpeed = deltaOut / elapsedTime * 1000; 60 | data.inSpeed = deltaIn / elapsedTime * 1000; 61 | } else { 62 | data.outSpeed = 0; 63 | data.inSpeed = 0; 64 | } 65 | 66 | callback(data); 67 | }); 68 | }; 69 | 70 | Memory.history = true; 71 | Memory.maxHistory = 3600; 72 | Memory.frequency = 20; 73 | 74 | module.exports = Memory; 75 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --transition: all ease-in-out 200ms; 3 | } 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | } 10 | 11 | body { 12 | font-family: "Open Sans", sans-serif; 13 | font-weight: 300; 14 | transition: var(--transition); 15 | } 16 | 17 | .theme-button { 18 | cursor: pointer; 19 | } 20 | 21 | .header { 22 | color: #2196f3; 23 | padding: 20px; 24 | font-size: 22px; 25 | font-weight: bold; 26 | position: fixed; 27 | width: 100%; 28 | top: 0; 29 | left: 0; 30 | z-index: 1; 31 | transition: var(--transition); 32 | } 33 | 34 | .container { 35 | width: 960px; 36 | margin: 0 auto; 37 | padding: 80px 0 0 10px; 38 | } 39 | 40 | .widget { 41 | margin: 0 10px 10px 0; 42 | text-align: center; 43 | display: inline-block; 44 | width: 100%; 45 | vertical-align: top; 46 | float: left; 47 | border-radius: 8px; 48 | overflow: hidden; 49 | transition: var(--transition); 50 | } 51 | 52 | .widget-content { 53 | padding: 20px; 54 | color: #2196f3; 55 | font-weight: 500; 56 | position: relative; 57 | } 58 | 59 | .widget-content .pie { 60 | text-align: left; 61 | color: #888; 62 | } 63 | 64 | .widget-content .legend { 65 | display: inline-block; 66 | margin-left: 40px; 67 | vertical-align: top; 68 | max-width: 292px; 69 | white-space: nowrap; 70 | overflow: hidden; 71 | } 72 | 73 | .widget-content .legend ul { 74 | list-style-type: none; 75 | line-height: 56px; 76 | cursor: default; 77 | } 78 | 79 | .widget-content .legend ul li span { 80 | width: 40px; 81 | height: 5px; 82 | display: inline-block; 83 | margin-right: 15px; 84 | vertical-align: middle; 85 | } 86 | 87 | .widget-content .legend ul li { 88 | position: relative; 89 | } 90 | 91 | .widget.one { 92 | width: 306px; 93 | } 94 | 95 | .widget.two { 96 | width: 624px; 97 | } 98 | 99 | .widget.three { 100 | width: 940px; 101 | } 102 | 103 | .widget-title { 104 | font-weight: 500; 105 | font-size: 18px; 106 | text-transform: uppercase; 107 | text-align: left; 108 | padding: 14px 20px; 109 | transition: var(--transition); 110 | } 111 | 112 | .title-container { 113 | width: 960px; 114 | margin: 0 auto; 115 | padding: 0 10px; 116 | display: flex; 117 | justify-content: space-between; 118 | } 119 | 120 | .tooltip { 121 | position: absolute; 122 | top: 190px; 123 | left: 610px; 124 | padding: 5px 10px; 125 | border-radius: 2px; 126 | white-space: nowrap; 127 | pointer-events: none; 128 | display: none; 129 | line-height: normal; 130 | max-width: 800px; 131 | white-space: pre-wrap; 132 | word-wrap: break-word; 133 | transition: var(--transition); 134 | } 135 | -------------------------------------------------------------------------------- /public/js/widgets/basepiewidget.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var COLORS = ['#F44336', '#3F51B5', '#4CAF50', '#00BCD4', '#FFEB3B']; 3 | 4 | var BasePieWidget = function(widget) { 5 | this.div = document.createElement('div'); 6 | this.div.className = 'pie'; 7 | 8 | this.canvas = document.createElement('canvas'); 9 | this.canvas.width = 250; 10 | this.canvas.height = 250; 11 | this.ctx = this.canvas.getContext('2d'); 12 | 13 | this.legendDiv = document.createElement('div'); 14 | this.legendDiv.className = 'legend'; 15 | 16 | this.div.appendChild(this.canvas); 17 | this.div.appendChild(this.legendDiv); 18 | 19 | this.numPoints = 0; 20 | }; 21 | BasePieWidget.prototype = Object.create(BaseWidget.prototype); 22 | 23 | BasePieWidget.prototype.getElement = function() { 24 | return this.div; 25 | }; 26 | 27 | BasePieWidget.prototype.getWidthClass = function() { 28 | return "two"; 29 | }; 30 | 31 | /* 32 | * Override to specify a suffix on the values in the chart 33 | * such as MB, or kB/s 34 | */ 35 | BasePieWidget.prototype.getSuffix = function() { 36 | return ""; 37 | }; 38 | 39 | BasePieWidget.prototype.initialize = function(history) { 40 | this.addData(history); 41 | }; 42 | 43 | BasePieWidget.prototype.getChartData = function(data) { 44 | throw "Children must override"; 45 | }; 46 | 47 | BasePieWidget.prototype.addData = function(data) { 48 | var index = this.numPoints++; 49 | 50 | var data = this.getChartData(data); 51 | 52 | if (!data) { 53 | return; 54 | } 55 | 56 | this.chart && this.chart.destroy(); 57 | 58 | this.chart = new Chart(this.ctx).Doughnut(data, { 59 | animateScale: index === 0, 60 | animateRotate : index === 0, 61 | tooltipTemplate: "<%= fillColor %>|<%= label %>: <%= value %>" 62 | + this.getSuffix(), 63 | customTooltips: function(tooltip) { 64 | if (!tooltip) { 65 | $(".tooltip").hide(); 66 | return; 67 | } 68 | 69 | var textSplit = tooltip.text.split("|"); 70 | var color = textSplit[0]; 71 | 72 | tooltip.labels = [textSplit[1]]; 73 | // tooltip.datasetLabels = ['']; 74 | tooltip.colors = [color]; 75 | 76 | window.moveTooltip(tooltip); 77 | }.bind(this), 78 | legendTemplate: 79 | '' 89 | }); 90 | 91 | this.legendDiv.innerHTML = this.chart.generateLegend(); 92 | }; 93 | 94 | 95 | window.BasePieWidget = BasePieWidget; 96 | })(); 97 | -------------------------------------------------------------------------------- /public/js/script.js: -------------------------------------------------------------------------------- 1 | var socket = io(); 2 | 3 | window.moveTooltip = function (tooltip, element, middleX) { 4 | var $tooltip = $('.tooltip'); 5 | if (!tooltip) { 6 | $tooltip.hide(); 7 | return; 8 | } 9 | 10 | if (!$tooltip.length) { 11 | $tooltip = $("
").addClass("tooltip"); 12 | } 13 | 14 | if (element) { 15 | element.appendChild($tooltip[0]); 16 | } else { 17 | tooltip.chart.canvas.parentNode.appendChild($tooltip[0]); 18 | } 19 | 20 | $tooltip.html(''); 21 | 22 | var values = tooltip.labels.map(function (label, index) { 23 | return { 24 | label: label, 25 | datasetLabel: tooltip.datasetLabels && tooltip.datasetLabels[index], 26 | color: tooltip.colors[index], 27 | }; 28 | }); 29 | 30 | values.sort(function (a, b) { 31 | return parseInt(b.label) - parseInt(a.label); 32 | }); 33 | 34 | values.forEach(function (value) { 35 | var label = value.label; 36 | if (value.datasetLabel) { 37 | label = value.datasetLabel + ': ' + label; 38 | } 39 | 40 | var $label = $('
') 41 | .text(label) 42 | .css({ 43 | color: value.color, 44 | }); 45 | $tooltip.append($label); 46 | }); 47 | 48 | $tooltip.css({ 49 | top: tooltip.y - 20 * (values && values.length || 1), 50 | left: tooltip.x + 20 - 51 | (tooltip.x > (middleX || 500) ? $tooltip.width() + 20 : 0), 52 | }); 53 | 54 | $tooltip.show(); 55 | }; 56 | 57 | $(function () { 58 | Chart.defaults.global.animation = false; 59 | 60 | var container = document.getElementsByClassName('container')[0]; 61 | 62 | var createWidget = function (Widget) { 63 | var widget = document.createElement('div'); 64 | container.appendChild(widget); 65 | 66 | var widgetObj = new Widget(widget); 67 | 68 | widget.className = 'widget ' + widgetObj.getWidthClass(); 69 | 70 | var titleEle = document.createElement('div'); 71 | titleEle.className = 'widget-title'; 72 | 73 | titleEle.innerText = widgetObj.getTitle(); 74 | 75 | var contEle = document.createElement('div'); 76 | contEle.className = 'widget-content'; 77 | 78 | widget.appendChild(titleEle); 79 | widget.appendChild(contEle); 80 | 81 | contEle.appendChild(widgetObj.getElement()); 82 | 83 | return widgetObj; 84 | }; 85 | 86 | var widgetClasses = [ 87 | CPUChartWidget, 88 | FreememWidget, 89 | NetworkWidget, 90 | ProcessCPUWidget, 91 | MemoryWidget, 92 | UptimeWidget, 93 | CPUUsageWidget, 94 | ProcessMemoryWidget, 95 | TempsWidget, 96 | ]; 97 | 98 | var widgets = widgetClasses.map(function (Widget) { 99 | return createWidget(Widget); 100 | }); 101 | 102 | socket.on('history', function (history) { 103 | widgets.forEach(function (widget) { 104 | widget.initialize(history); 105 | }); 106 | }); 107 | 108 | socket.on('data', function (data) { 109 | widgets.forEach(function (widget) { 110 | widget.addData(data); 111 | }); 112 | }); 113 | 114 | // Legend tooltips 115 | $(".legend").on("mouseover", "li", function (event) { 116 | var rect = this.getClientRects()[0]; 117 | window.moveTooltip({ 118 | labels: [this.textContent], 119 | colors: [$(this).find("span").css("background-color")], 120 | x: rect.left + 30, 121 | y: rect.top + 30 + window.scrollY, 122 | }, document.body, 10000); 123 | }); 124 | 125 | $(".legend").on("mouseout", "li", function (event) { 126 | $(".tooltip").hide(); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /monitor.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var os = require('os'); 3 | var async = require('async'); 4 | 5 | // Constants 6 | var EMPTY_CALLBACK = function (callback) { 7 | callback(); 8 | }; 9 | 10 | // Monitors 11 | var monitors = { 12 | memory: require('./monitors/memory'), 13 | uptime: require('./monitors/uptime'), 14 | network: require('./monitors/network'), 15 | processes: require('./monitors/processes'), 16 | cpu: require('./monitors/cpu'), 17 | temps: require('./monitors/temps'), 18 | }; 19 | 20 | // XXX: Hacky way to prevent us from having to call the callback 21 | // with (null, result) from within the monitors 22 | for (var key in monitors) { 23 | var monitor = monitors[key]; 24 | 25 | (function (monitor) { 26 | monitor._asyncMonitor = function (history, callback) { 27 | monitor.monitor(history, function (result) { 28 | callback(null, result); 29 | }); 30 | }; 31 | })(monitor); 32 | } 33 | 34 | var monitorArray = []; 35 | for (var key in monitors) { 36 | var monitor = monitors[key]; 37 | 38 | monitorArray.push({ 39 | monitor: monitor, 40 | key: key, 41 | }); 42 | } 43 | 44 | monitors = monitorArray; 45 | 46 | // Event stuff 47 | var _eventHandlers = {}; 48 | var _trigger = function (eventName, data) { 49 | if (!_eventHandlers[eventName]) { 50 | return; 51 | } 52 | 53 | _eventHandlers[eventName].forEach(function (callback) { 54 | callback && callback(data); 55 | }); 56 | }; 57 | 58 | // Main 59 | var Monitor = function () { 60 | this._history = { 61 | memory: [], 62 | network: [], 63 | processes: [], 64 | temps: [], 65 | }; 66 | 67 | monitors.forEach(function (monitor) { 68 | if (monitor.monitor.history) { 69 | this._history[monitor.key] = []; 70 | } 71 | }, this); 72 | 73 | this._numData = 0; 74 | 75 | this._monitor(); 76 | }; 77 | 78 | Monitor.prototype.on = function (eventName, callback) { 79 | _eventHandlers[eventName] = _eventHandlers[eventName] || []; 80 | _eventHandlers[eventName].push(callback); 81 | }; 82 | 83 | Monitor.prototype._getData = function (callback) { 84 | var numData = this._numData++; 85 | 86 | var data = {}; 87 | 88 | var monitorAsyncFuncs = monitors.map(function (monitor) { 89 | if (numData % monitor.monitor.frequency === 0) { 90 | return monitor.monitor._asyncMonitor.bind( 91 | null, 92 | this._history[monitor.key] 93 | ); 94 | } else { 95 | return EMPTY_CALLBACK; 96 | } 97 | }, this); 98 | 99 | async.parallel(monitorAsyncFuncs, function (error, results) { 100 | results.forEach(function (result, index) { 101 | if (result === undefined || result === null) { 102 | return; 103 | } 104 | 105 | var monitor = monitors[index]; 106 | var key = monitors[index].key; 107 | monitor = monitor.monitor; 108 | 109 | data[key] = result; 110 | data[key].timestamp = Date.now(); 111 | 112 | if (monitor.history) { 113 | this._history[key].push(result); 114 | } else { 115 | this._history[key] = result; 116 | } 117 | 118 | var maxNumHistoryElements = (monitor.maxHistory / monitor.frequency); 119 | if (this._history[key].length > maxNumHistoryElements) { 120 | this._history[key].splice(0, 1); 121 | } 122 | }, this); 123 | callback(data); 124 | }.bind(this)); 125 | }; 126 | 127 | Monitor.prototype.getHistory = function () { 128 | return this._history; 129 | }; 130 | 131 | Monitor.prototype._monitor = function () { 132 | this._getData(function (data) { 133 | _trigger('data', data); 134 | }); 135 | 136 | setTimeout(this._monitor.bind(this), 1000); 137 | }; 138 | 139 | module.exports = new Monitor(); 140 | -------------------------------------------------------------------------------- /public/js/widgets/basechartwidget.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var BaseChartWidget = function(widget) { 3 | this.canvas = document.createElement('canvas'); 4 | this.canvas.width = widget.clientWidth - 40; 5 | this.canvas.height = 280; 6 | this.ctx = this.canvas.getContext('2d'); 7 | 8 | this.numPoints = 0; 9 | }; 10 | BaseChartWidget.prototype = Object.create(BaseWidget.prototype); 11 | 12 | BaseChartWidget.prototype.getElement = function() { 13 | return this.canvas; 14 | }; 15 | 16 | BaseChartWidget.prototype.getColors = function() { 17 | return ['#000000']; 18 | }, 19 | 20 | BaseChartWidget.prototype.getFillColors = function() { 21 | return ['#AAAAAA']; 22 | }; 23 | 24 | BaseChartWidget.prototype.getLabels = function() { 25 | return ['Label']; 26 | }; 27 | 28 | /* 29 | * Override to specify a suffix on the values in the chart 30 | * such as MB, or kB/s 31 | */ 32 | BaseChartWidget.prototype.getSuffix = function() { 33 | return ""; 34 | }; 35 | 36 | /* 37 | * Should return an object like: 38 | * { 39 | * values: [[1, 2, 3], [2, 6, 1]], 40 | * labels: ['', 'test', ''], 41 | * } 42 | */ 43 | BaseChartWidget.prototype.getPointsFromHistory = function(history) { 44 | throw "Children must override"; 45 | }; 46 | 47 | /* 48 | * Should return an integer 49 | */ 50 | BaseChartWidget.prototype.getValuesFromData = function(data) { 51 | throw "Children must override"; 52 | }; 53 | 54 | BaseChartWidget.prototype.getLabelFromData = function(data) { 55 | return ""; 56 | } 57 | 58 | BaseChartWidget.prototype.initialize = function(history) { 59 | var dataPoints = this.getPointsFromHistory(history); 60 | var labels = dataPoints.labels; 61 | 62 | var datasets = dataPoints.values.map(function(dataset, index) { 63 | return { 64 | fillColor: this.getFillColors()[index], 65 | strokeColor: this.getColors()[index], 66 | pointColor: 'rgba(0,0,0,0)', 67 | pointStrokeColor: 'rgba(0,0,0,0)', 68 | data: dataset, 69 | }; 70 | }, this); 71 | 72 | var chartData = { 73 | labels: labels, 74 | datasets: datasets, 75 | }; 76 | 77 | this.chart = new Chart(this.ctx).Line(chartData, { 78 | bezierCurveTension: 0.3, 79 | animationSteps: 30, 80 | scaleLabel: "<%= value %> " + this.getSuffix(), 81 | pointDotRadius: 4, 82 | scaleShowVerticalLines: false, 83 | showTooltips: false, 84 | }); 85 | 86 | $(this.canvas).on('mousemove', function(event) { 87 | var mouseX = event.originalEvent.offsetX || event.originalEvent.layerX; 88 | var mouseY = event.originalEvent.offsetY || event.originalEvent.layerY; 89 | 90 | this.chart.datasets.forEach(function(dataset) { 91 | dataset.points.forEach(function(point) { 92 | point.fillColor = 'rgba(0,0,0,0)'; 93 | }); 94 | }); 95 | 96 | var points = this.chart.datasets[0].points; 97 | var sortedPoints = points.map(function(point, index) { 98 | return { 99 | point: point, 100 | index: index, 101 | }; 102 | }); 103 | 104 | sortedPoints.sort(function(a, b) { 105 | var aDist = Math.abs(mouseX - a.point.x); 106 | var bDist = Math.abs(mouseX - b.point.x); 107 | 108 | return aDist - bDist; 109 | }); 110 | 111 | var closestPoint = sortedPoints[0]; 112 | 113 | this.chart.datasets.forEach(function(dataset, datasetIndex) { 114 | var index = sortedPoints[0].index; 115 | dataset.points[index].fillColor = this.getColors()[datasetIndex]; 116 | }, this); 117 | 118 | this.chart.render(true); 119 | 120 | var labels = this.chart.datasets.map(function(dataset) { 121 | return dataset.points[closestPoint.index].value + 122 | ' ' + this.getSuffix(); 123 | }, this); 124 | 125 | moveTooltip({ 126 | chart: this.chart.chart, 127 | labels: labels, 128 | colors: this.getColors(), 129 | datasetLabels: this.getLabels(), 130 | x: closestPoint.point.x, 131 | y: closestPoint.point.y, 132 | }); 133 | }.bind(this)); 134 | 135 | 136 | $(this.canvas).on('mouseout', function() { 137 | // Reset all datasets 138 | this.chart.datasets.forEach(function(dataset) { 139 | dataset.points.forEach(function(point) { 140 | point.fillColor = 'rgba(0,0,0,0)'; 141 | }); 142 | }); 143 | 144 | this.chart.render(true); 145 | 146 | moveTooltip(false); 147 | }.bind(this)); 148 | }; 149 | 150 | BaseChartWidget.prototype.addData = function(data) { 151 | var values = this.getValuesFromData(data); 152 | 153 | if (!values) { 154 | return; 155 | } 156 | 157 | var index = this.numPoints++; 158 | 159 | this.chart.addData( 160 | values, 161 | index % 5 === 0 ? this.getLabelFromData(data) : '' 162 | ); 163 | }; 164 | 165 | window.BaseChartWidget = BaseChartWidget; 166 | })(); 167 | -------------------------------------------------------------------------------- /public/js/lib/moment.min.js: -------------------------------------------------------------------------------- 1 | //! moment.js 2 | //! version : 2.9.0 3 | //! authors : Tim Wood, Iskren Chernev, Moment.js contributors 4 | //! license : MIT 5 | //! momentjs.com 6 | (function(a){function b(a,b,c){switch(arguments.length){case 2:return null!=a?a:b;case 3:return null!=a?a:null!=b?b:c;default:throw new Error("Implement me")}}function c(a,b){return Bb.call(a,b)}function d(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function e(a){vb.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+a)}function f(a,b){var c=!0;return o(function(){return c&&(e(a),c=!1),b.apply(this,arguments)},b)}function g(a,b){sc[a]||(e(b),sc[a]=!0)}function h(a,b){return function(c){return r(a.call(this,c),b)}}function i(a,b){return function(c){return this.localeData().ordinal(a.call(this,c),b)}}function j(a,b){var c,d,e=12*(b.year()-a.year())+(b.month()-a.month()),f=a.clone().add(e,"months");return 0>b-f?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)}function k(a,b,c){var d;return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&12>b&&(b+=12),d||12!==b||(b=0),b):b}function l(){}function m(a,b){b!==!1&&H(a),p(this,a),this._d=new Date(+a._d),uc===!1&&(uc=!0,vb.updateOffset(this),uc=!1)}function n(a){var b=A(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;this._milliseconds=+k+1e3*j+6e4*i+36e5*h,this._days=+g+7*f,this._months=+e+3*d+12*c,this._data={},this._locale=vb.localeData(),this._bubble()}function o(a,b){for(var d in b)c(b,d)&&(a[d]=b[d]);return c(b,"toString")&&(a.toString=b.toString),c(b,"valueOf")&&(a.valueOf=b.valueOf),a}function p(a,b){var c,d,e;if("undefined"!=typeof b._isAMomentObject&&(a._isAMomentObject=b._isAMomentObject),"undefined"!=typeof b._i&&(a._i=b._i),"undefined"!=typeof b._f&&(a._f=b._f),"undefined"!=typeof b._l&&(a._l=b._l),"undefined"!=typeof b._strict&&(a._strict=b._strict),"undefined"!=typeof b._tzm&&(a._tzm=b._tzm),"undefined"!=typeof b._isUTC&&(a._isUTC=b._isUTC),"undefined"!=typeof b._offset&&(a._offset=b._offset),"undefined"!=typeof b._pf&&(a._pf=b._pf),"undefined"!=typeof b._locale&&(a._locale=b._locale),Kb.length>0)for(c in Kb)d=Kb[c],e=b[d],"undefined"!=typeof e&&(a[d]=e);return a}function q(a){return 0>a?Math.ceil(a):Math.floor(a)}function r(a,b,c){for(var d=""+Math.abs(a),e=a>=0;d.lengthd;d++)(c&&a[d]!==b[d]||!c&&C(a[d])!==C(b[d]))&&g++;return g+f}function z(a){if(a){var b=a.toLowerCase().replace(/(.)s$/,"$1");a=lc[a]||mc[b]||b}return a}function A(a){var b,d,e={};for(d in a)c(a,d)&&(b=z(d),b&&(e[b]=a[d]));return e}function B(b){var c,d;if(0===b.indexOf("week"))c=7,d="day";else{if(0!==b.indexOf("month"))return;c=12,d="month"}vb[b]=function(e,f){var g,h,i=vb._locale[b],j=[];if("number"==typeof e&&(f=e,e=a),h=function(a){var b=vb().utc().set(d,a);return i.call(vb._locale,b,e||"")},null!=f)return h(f);for(g=0;c>g;g++)j.push(h(g));return j}}function C(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function D(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function E(a,b,c){return jb(vb([a,11,31+b-c]),b,c).week}function F(a){return G(a)?366:365}function G(a){return a%4===0&&a%100!==0||a%400===0}function H(a){var b;a._a&&-2===a._pf.overflow&&(b=a._a[Db]<0||a._a[Db]>11?Db:a._a[Eb]<1||a._a[Eb]>D(a._a[Cb],a._a[Db])?Eb:a._a[Fb]<0||a._a[Fb]>24||24===a._a[Fb]&&(0!==a._a[Gb]||0!==a._a[Hb]||0!==a._a[Ib])?Fb:a._a[Gb]<0||a._a[Gb]>59?Gb:a._a[Hb]<0||a._a[Hb]>59?Hb:a._a[Ib]<0||a._a[Ib]>999?Ib:-1,a._pf._overflowDayOfYear&&(Cb>b||b>Eb)&&(b=Eb),a._pf.overflow=b)}function I(b){return null==b._isValid&&(b._isValid=!isNaN(b._d.getTime())&&b._pf.overflow<0&&!b._pf.empty&&!b._pf.invalidMonth&&!b._pf.nullInput&&!b._pf.invalidFormat&&!b._pf.userInvalidated,b._strict&&(b._isValid=b._isValid&&0===b._pf.charsLeftOver&&0===b._pf.unusedTokens.length&&b._pf.bigHour===a)),b._isValid}function J(a){return a?a.toLowerCase().replace("_","-"):a}function K(a){for(var b,c,d,e,f=0;f0;){if(d=L(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&y(e,c,!0)>=b-1)break;b--}f++}return null}function L(a){var b=null;if(!Jb[a]&&Lb)try{b=vb.locale(),require("./locale/"+a),vb.locale(b)}catch(c){}return Jb[a]}function M(a,b){var c,d;return b._isUTC?(c=b.clone(),d=(vb.isMoment(a)||x(a)?+a:+vb(a))-+c,c._d.setTime(+c._d+d),vb.updateOffset(c,!1),c):vb(a).local()}function N(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function O(a){var b,c,d=a.match(Pb);for(b=0,c=d.length;c>b;b++)d[b]=rc[d[b]]?rc[d[b]]:N(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function P(a,b){return a.isValid()?(b=Q(b,a.localeData()),nc[b]||(nc[b]=O(b)),nc[b](a)):a.localeData().invalidDate()}function Q(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Qb.lastIndex=0;d>=0&&Qb.test(a);)a=a.replace(Qb,c),Qb.lastIndex=0,d-=1;return a}function R(a,b){var c,d=b._strict;switch(a){case"Q":return _b;case"DDDD":return bc;case"YYYY":case"GGGG":case"gggg":return d?cc:Tb;case"Y":case"G":case"g":return ec;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return d?dc:Ub;case"S":if(d)return _b;case"SS":if(d)return ac;case"SSS":if(d)return bc;case"DDD":return Sb;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Wb;case"a":case"A":return b._locale._meridiemParse;case"x":return Zb;case"X":return $b;case"Z":case"ZZ":return Xb;case"T":return Yb;case"SSSS":return Vb;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return d?ac:Rb;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Rb;case"Do":return d?b._locale._ordinalParse:b._locale._ordinalParseLenient;default:return c=new RegExp($(Z(a.replace("\\","")),"i"))}}function S(a){a=a||"";var b=a.match(Xb)||[],c=b[b.length-1]||[],d=(c+"").match(jc)||["-",0,0],e=+(60*d[1])+C(d[2]);return"+"===d[0]?e:-e}function T(a,b,c){var d,e=c._a;switch(a){case"Q":null!=b&&(e[Db]=3*(C(b)-1));break;case"M":case"MM":null!=b&&(e[Db]=C(b)-1);break;case"MMM":case"MMMM":d=c._locale.monthsParse(b,a,c._strict),null!=d?e[Db]=d:c._pf.invalidMonth=b;break;case"D":case"DD":null!=b&&(e[Eb]=C(b));break;case"Do":null!=b&&(e[Eb]=C(parseInt(b.match(/\d{1,2}/)[0],10)));break;case"DDD":case"DDDD":null!=b&&(c._dayOfYear=C(b));break;case"YY":e[Cb]=vb.parseTwoDigitYear(b);break;case"YYYY":case"YYYYY":case"YYYYYY":e[Cb]=C(b);break;case"a":case"A":c._meridiem=b;break;case"h":case"hh":c._pf.bigHour=!0;case"H":case"HH":e[Fb]=C(b);break;case"m":case"mm":e[Gb]=C(b);break;case"s":case"ss":e[Hb]=C(b);break;case"S":case"SS":case"SSS":case"SSSS":e[Ib]=C(1e3*("0."+b));break;case"x":c._d=new Date(C(b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=S(b);break;case"dd":case"ddd":case"dddd":d=c._locale.weekdaysParse(b),null!=d?(c._w=c._w||{},c._w.d=d):c._pf.invalidWeekday=b;break;case"w":case"ww":case"W":case"WW":case"d":case"e":case"E":a=a.substr(0,1);case"gggg":case"GGGG":case"GGGGG":a=a.substr(0,2),b&&(c._w=c._w||{},c._w[a]=C(b));break;case"gg":case"GG":c._w=c._w||{},c._w[a]=vb.parseTwoDigitYear(b)}}function U(a){var c,d,e,f,g,h,i;c=a._w,null!=c.GG||null!=c.W||null!=c.E?(g=1,h=4,d=b(c.GG,a._a[Cb],jb(vb(),1,4).year),e=b(c.W,1),f=b(c.E,1)):(g=a._locale._week.dow,h=a._locale._week.doy,d=b(c.gg,a._a[Cb],jb(vb(),g,h).year),e=b(c.w,1),null!=c.d?(f=c.d,g>f&&++e):f=null!=c.e?c.e+g:g),i=kb(d,e,f,h,g),a._a[Cb]=i.year,a._dayOfYear=i.dayOfYear}function V(a){var c,d,e,f,g=[];if(!a._d){for(e=X(a),a._w&&null==a._a[Eb]&&null==a._a[Db]&&U(a),a._dayOfYear&&(f=b(a._a[Cb],e[Cb]),a._dayOfYear>F(f)&&(a._pf._overflowDayOfYear=!0),d=fb(f,0,a._dayOfYear),a._a[Db]=d.getUTCMonth(),a._a[Eb]=d.getUTCDate()),c=0;3>c&&null==a._a[c];++c)a._a[c]=g[c]=e[c];for(;7>c;c++)a._a[c]=g[c]=null==a._a[c]?2===c?1:0:a._a[c];24===a._a[Fb]&&0===a._a[Gb]&&0===a._a[Hb]&&0===a._a[Ib]&&(a._nextDay=!0,a._a[Fb]=0),a._d=(a._useUTC?fb:eb).apply(null,g),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[Fb]=24)}}function W(a){var b;a._d||(b=A(a._i),a._a=[b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],V(a))}function X(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function Y(b){if(b._f===vb.ISO_8601)return void ab(b);b._a=[],b._pf.empty=!0;var c,d,e,f,g,h=""+b._i,i=h.length,j=0;for(e=Q(b._f,b._locale).match(Pb)||[],c=0;c0&&b._pf.unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),j+=d.length),rc[f]?(d?b._pf.empty=!1:b._pf.unusedTokens.push(f),T(f,d,b)):b._strict&&!d&&b._pf.unusedTokens.push(f);b._pf.charsLeftOver=i-j,h.length>0&&b._pf.unusedInput.push(h),b._pf.bigHour===!0&&b._a[Fb]<=12&&(b._pf.bigHour=a),b._a[Fb]=k(b._locale,b._a[Fb],b._meridiem),V(b),H(b)}function Z(a){return a.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e})}function $(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function _(a){var b,c,e,f,g;if(0===a._f.length)return a._pf.invalidFormat=!0,void(a._d=new Date(0/0));for(f=0;fg)&&(e=g,c=b));o(a,c||b)}function ab(a){var b,c,d=a._i,e=fc.exec(d);if(e){for(a._pf.iso=!0,b=0,c=hc.length;c>b;b++)if(hc[b][1].exec(d)){a._f=hc[b][0]+(e[6]||" ");break}for(b=0,c=ic.length;c>b;b++)if(ic[b][1].exec(d)){a._f+=ic[b][0];break}d.match(Xb)&&(a._f+="Z"),Y(a)}else a._isValid=!1}function bb(a){ab(a),a._isValid===!1&&(delete a._isValid,vb.createFromInputFallback(a))}function cb(a,b){var c,d=[];for(c=0;ca&&h.setFullYear(a),h}function fb(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function gb(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function hb(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function ib(a,b,c){var d=vb.duration(a).abs(),e=Ab(d.as("s")),f=Ab(d.as("m")),g=Ab(d.as("h")),h=Ab(d.as("d")),i=Ab(d.as("M")),j=Ab(d.as("y")),k=e0,k[4]=c,hb.apply({},k)}function jb(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=vb(a).add(f,"d"),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function kb(a,b,c,d,e){var f,g,h=fb(a,0,1).getUTCDay();return h=0===h?7:h,c=null!=c?c:e,f=e-h+(h>d?7:0)-(e>h?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:F(a-1)+g}}function lb(b){var c,d=b._i,e=b._f;return b._locale=b._locale||vb.localeData(b._l),null===d||e===a&&""===d?vb.invalid({nullInput:!0}):("string"==typeof d&&(b._i=d=b._locale.preparse(d)),vb.isMoment(d)?new m(d,!0):(e?w(e)?_(b):Y(b):db(b),c=new m(b),c._nextDay&&(c.add(1,"d"),c._nextDay=a),c))}function mb(a,b){var c,d;if(1===b.length&&w(b[0])&&(b=b[0]),!b.length)return vb();for(c=b[0],d=1;d=0?"+":"-";return b+r(Math.abs(a),6)},gg:function(){return r(this.weekYear()%100,2)},gggg:function(){return r(this.weekYear(),4)},ggggg:function(){return r(this.weekYear(),5)},GG:function(){return r(this.isoWeekYear()%100,2)},GGGG:function(){return r(this.isoWeekYear(),4)},GGGGG:function(){return r(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.localeData().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.localeData().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return C(this.milliseconds()/100)},SS:function(){return r(C(this.milliseconds()/10),2)},SSS:function(){return r(this.milliseconds(),3)},SSSS:function(){return r(this.milliseconds(),3)},Z:function(){var a=this.utcOffset(),b="+";return 0>a&&(a=-a,b="-"),b+r(C(a/60),2)+":"+r(C(a)%60,2)},ZZ:function(){var a=this.utcOffset(),b="+";return 0>a&&(a=-a,b="-"),b+r(C(a/60),2)+r(C(a)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},x:function(){return this.valueOf()},X:function(){return this.unix()},Q:function(){return this.quarter()}},sc={},tc=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"],uc=!1;pc.length;)xb=pc.pop(),rc[xb+"o"]=i(rc[xb],xb);for(;qc.length;)xb=qc.pop(),rc[xb+xb]=h(rc[xb],2);rc.DDDD=h(rc.DDD,3),o(l.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a,b,c){var d,e,f;for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),d=0;12>d;d++){if(e=vb.utc([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=vb([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY LT",LLLL:"dddd, MMMM D, YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b,c){var d=this._calendar[a];return"function"==typeof d?d.apply(b,[c]):d},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",_ordinalParse:/\d{1,2}/,preparse:function(a){return a},postformat:function(a){return a},week:function(a){return jb(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},firstDayOfWeek:function(){return this._week.dow},firstDayOfYear:function(){return this._week.doy},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),vb=function(b,c,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._i=b,g._f=c,g._l=e,g._strict=f,g._isUTC=!1,g._pf=d(),lb(g)},vb.suppressDeprecationWarnings=!1,vb.createFromInputFallback=f("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}),vb.min=function(){var a=[].slice.call(arguments,0);return mb("isBefore",a)},vb.max=function(){var a=[].slice.call(arguments,0);return mb("isAfter",a)},vb.utc=function(b,c,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._useUTC=!0,g._isUTC=!0,g._l=e,g._i=b,g._f=c,g._strict=f,g._pf=d(),lb(g).utc()},vb.unix=function(a){return vb(1e3*a)},vb.duration=function(a,b){var d,e,f,g,h=a,i=null;return vb.isDuration(a)?h={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(h={},b?h[b]=a:h.milliseconds=a):(i=Nb.exec(a))?(d="-"===i[1]?-1:1,h={y:0,d:C(i[Eb])*d,h:C(i[Fb])*d,m:C(i[Gb])*d,s:C(i[Hb])*d,ms:C(i[Ib])*d}):(i=Ob.exec(a))?(d="-"===i[1]?-1:1,f=function(a){var b=a&&parseFloat(a.replace(",","."));return(isNaN(b)?0:b)*d},h={y:f(i[2]),M:f(i[3]),d:f(i[4]),h:f(i[5]),m:f(i[6]),s:f(i[7]),w:f(i[8])}):null==h?h={}:"object"==typeof h&&("from"in h||"to"in h)&&(g=t(vb(h.from),vb(h.to)),h={},h.ms=g.milliseconds,h.M=g.months),e=new n(h),vb.isDuration(a)&&c(a,"_locale")&&(e._locale=a._locale),e},vb.version=yb,vb.defaultFormat=gc,vb.ISO_8601=function(){},vb.momentProperties=Kb,vb.updateOffset=function(){},vb.relativeTimeThreshold=function(b,c){return oc[b]===a?!1:c===a?oc[b]:(oc[b]=c,!0)},vb.lang=f("moment.lang is deprecated. Use moment.locale instead.",function(a,b){return vb.locale(a,b)}),vb.locale=function(a,b){var c;return a&&(c="undefined"!=typeof b?vb.defineLocale(a,b):vb.localeData(a),c&&(vb.duration._locale=vb._locale=c)),vb._locale._abbr},vb.defineLocale=function(a,b){return null!==b?(b.abbr=a,Jb[a]||(Jb[a]=new l),Jb[a].set(b),vb.locale(a),Jb[a]):(delete Jb[a],null)},vb.langData=f("moment.langData is deprecated. Use moment.localeData instead.",function(a){return vb.localeData(a)}),vb.localeData=function(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return vb._locale;if(!w(a)){if(b=L(a))return b;a=[a]}return K(a)},vb.isMoment=function(a){return a instanceof m||null!=a&&c(a,"_isAMomentObject")},vb.isDuration=function(a){return a instanceof n};for(xb=tc.length-1;xb>=0;--xb)B(tc[xb]);vb.normalizeUnits=function(a){return z(a)},vb.invalid=function(a){var b=vb.utc(0/0);return null!=a?o(b._pf,a):b._pf.userInvalidated=!0,b},vb.parseZone=function(){return vb.apply(null,arguments).parseZone()},vb.parseTwoDigitYear=function(a){return C(a)+(C(a)>68?1900:2e3)},vb.isDate=x,o(vb.fn=m.prototype,{clone:function(){return vb(this)},valueOf:function(){return+this._d-6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var a=vb(this).utc();return 00:!1},parsingFlags:function(){return o({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(a){return this.utcOffset(0,a)},local:function(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(this._dateUtcOffset(),"m")),this},format:function(a){var b=P(this,a||vb.defaultFormat);return this.localeData().postformat(b)},add:u(1,"add"),subtract:u(-1,"subtract"),diff:function(a,b,c){var d,e,f=M(a,this),g=6e4*(f.utcOffset()-this.utcOffset());return b=z(b),"year"===b||"month"===b||"quarter"===b?(e=j(this,f),"quarter"===b?e/=3:"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:q(e)},from:function(a,b){return vb.duration({to:this,from:a}).locale(this.locale()).humanize(!b)},fromNow:function(a){return this.from(vb(),a)},calendar:function(a){var b=a||vb(),c=M(b,this).startOf("day"),d=this.diff(c,"days",!0),e=-6>d?"sameElse":-1>d?"lastWeek":0>d?"lastDay":1>d?"sameDay":2>d?"nextDay":7>d?"nextWeek":"sameElse";return this.format(this.localeData().calendar(e,this,vb(b)))},isLeapYear:function(){return G(this.year())},isDST:function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},day:function(a){var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=gb(a,this.localeData()),this.add(a-b,"d")):b},month:qb("Month",!0),startOf:function(a){switch(a=z(a)){case"year":this.month(0);case"quarter":case"month":this.date(1);case"week":case"isoWeek":case"day":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===a?this.weekday(0):"isoWeek"===a&&this.isoWeekday(1),"quarter"===a&&this.month(3*Math.floor(this.month()/3)),this},endOf:function(b){return b=z(b),b===a||"millisecond"===b?this:this.startOf(b).add(1,"isoWeek"===b?"week":b).subtract(1,"ms")},isAfter:function(a,b){var c;return b=z("undefined"!=typeof b?b:"millisecond"),"millisecond"===b?(a=vb.isMoment(a)?a:vb(a),+this>+a):(c=vb.isMoment(a)?+a:+vb(a),c<+this.clone().startOf(b))},isBefore:function(a,b){var c;return b=z("undefined"!=typeof b?b:"millisecond"),"millisecond"===b?(a=vb.isMoment(a)?a:vb(a),+a>+this):(c=vb.isMoment(a)?+a:+vb(a),+this.clone().endOf(b)a?this:a}),max:f("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(a){return a=vb.apply(null,arguments),a>this?this:a}),zone:f("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",function(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}),utcOffset:function(a,b){var c,d=this._offset||0;return null!=a?("string"==typeof a&&(a=S(a)),Math.abs(a)<16&&(a=60*a),!this._isUTC&&b&&(c=this._dateUtcOffset()),this._offset=a,this._isUTC=!0,null!=c&&this.add(c,"m"),d!==a&&(!b||this._changeInProgress?v(this,vb.duration(a-d,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,vb.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?d:this._dateUtcOffset()},isLocal:function(){return!this._isUTC},isUtcOffset:function(){return this._isUTC},isUtc:function(){return this._isUTC&&0===this._offset},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(S(this._i)),this},hasAlignedHourOffset:function(a){return a=a?vb(a).utcOffset():0,(this.utcOffset()-a)%60===0},daysInMonth:function(){return D(this.year(),this.month())},dayOfYear:function(a){var b=Ab((vb(this).startOf("day")-vb(this).startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")},quarter:function(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)},weekYear:function(a){var b=jb(this,this.localeData()._week.dow,this.localeData()._week.doy).year;return null==a?b:this.add(a-b,"y")},isoWeekYear:function(a){var b=jb(this,1,4).year;return null==a?b:this.add(a-b,"y")},week:function(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")},isoWeek:function(a){var b=jb(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")},weekday:function(a){var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},isoWeeksInYear:function(){return E(this.year(),1,4)},weeksInYear:function(){var a=this.localeData()._week;return E(this.year(),a.dow,a.doy)},get:function(a){return a=z(a),this[a]()},set:function(a,b){var c;if("object"==typeof a)for(c in a)this.set(c,a[c]);else a=z(a),"function"==typeof this[a]&&this[a](b);return this},locale:function(b){var c;return b===a?this._locale._abbr:(c=vb.localeData(b),null!=c&&(this._locale=c),this)},lang:f("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(b){return b===a?this.localeData():this.locale(b)}),localeData:function(){return this._locale},_dateUtcOffset:function(){return 15*-Math.round(this._d.getTimezoneOffset()/15)}}),vb.fn.millisecond=vb.fn.milliseconds=qb("Milliseconds",!1),vb.fn.second=vb.fn.seconds=qb("Seconds",!1),vb.fn.minute=vb.fn.minutes=qb("Minutes",!1),vb.fn.hour=vb.fn.hours=qb("Hours",!0),vb.fn.date=qb("Date",!0),vb.fn.dates=f("dates accessor is deprecated. Use date instead.",qb("Date",!0)),vb.fn.year=qb("FullYear",!0),vb.fn.years=f("years accessor is deprecated. Use year instead.",qb("FullYear",!0)),vb.fn.days=vb.fn.day,vb.fn.months=vb.fn.month,vb.fn.weeks=vb.fn.week,vb.fn.isoWeeks=vb.fn.isoWeek,vb.fn.quarters=vb.fn.quarter,vb.fn.toJSON=vb.fn.toISOString,vb.fn.isUTC=vb.fn.isUtc,o(vb.duration.fn=n.prototype,{_bubble:function(){var a,b,c,d=this._milliseconds,e=this._days,f=this._months,g=this._data,h=0;g.milliseconds=d%1e3,a=q(d/1e3),g.seconds=a%60,b=q(a/60),g.minutes=b%60,c=q(b/60),g.hours=c%24,e+=q(c/24),h=q(rb(e)),e-=q(sb(h)),f+=q(e/30),e%=30,h+=q(f/12),f%=12,g.days=e,g.months=f,g.years=h},abs:function(){return this._milliseconds=Math.abs(this._milliseconds),this._days=Math.abs(this._days),this._months=Math.abs(this._months),this._data.milliseconds=Math.abs(this._data.milliseconds),this._data.seconds=Math.abs(this._data.seconds),this._data.minutes=Math.abs(this._data.minutes),this._data.hours=Math.abs(this._data.hours),this._data.months=Math.abs(this._data.months),this._data.years=Math.abs(this._data.years),this},weeks:function(){return q(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*C(this._months/12) 7 | },humanize:function(a){var b=ib(this,!a,this.localeData());return a&&(b=this.localeData().pastFuture(+this,b)),this.localeData().postformat(b)},add:function(a,b){var c=vb.duration(a,b);return this._milliseconds+=c._milliseconds,this._days+=c._days,this._months+=c._months,this._bubble(),this},subtract:function(a,b){var c=vb.duration(a,b);return this._milliseconds-=c._milliseconds,this._days-=c._days,this._months-=c._months,this._bubble(),this},get:function(a){return a=z(a),this[a.toLowerCase()+"s"]()},as:function(a){var b,c;if(a=z(a),"month"===a||"year"===a)return b=this._days+this._milliseconds/864e5,c=this._months+12*rb(b),"month"===a?c:c/12;switch(b=this._days+Math.round(sb(this._months/12)),a){case"week":return b/7+this._milliseconds/6048e5;case"day":return b+this._milliseconds/864e5;case"hour":return 24*b+this._milliseconds/36e5;case"minute":return 24*b*60+this._milliseconds/6e4;case"second":return 24*b*60*60+this._milliseconds/1e3;case"millisecond":return Math.floor(24*b*60*60*1e3)+this._milliseconds;default:throw new Error("Unknown unit "+a)}},lang:vb.fn.lang,locale:vb.fn.locale,toIsoString:f("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",function(){return this.toISOString()}),toISOString:function(){var a=Math.abs(this.years()),b=Math.abs(this.months()),c=Math.abs(this.days()),d=Math.abs(this.hours()),e=Math.abs(this.minutes()),f=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(a?a+"Y":"")+(b?b+"M":"")+(c?c+"D":"")+(d||e||f?"T":"")+(d?d+"H":"")+(e?e+"M":"")+(f?f+"S":""):"P0D"},localeData:function(){return this._locale},toJSON:function(){return this.toISOString()}}),vb.duration.fn.toString=vb.duration.fn.toISOString;for(xb in kc)c(kc,xb)&&tb(xb.toLowerCase());vb.duration.fn.asMilliseconds=function(){return this.as("ms")},vb.duration.fn.asSeconds=function(){return this.as("s")},vb.duration.fn.asMinutes=function(){return this.as("m")},vb.duration.fn.asHours=function(){return this.as("h")},vb.duration.fn.asDays=function(){return this.as("d")},vb.duration.fn.asWeeks=function(){return this.as("weeks")},vb.duration.fn.asMonths=function(){return this.as("M")},vb.duration.fn.asYears=function(){return this.as("y")},vb.locale("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===C(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),Lb?module.exports=vb:"function"==typeof define&&define.amd?(define(function(a,b,c){return c.config&&c.config()&&c.config().noGlobal===!0&&(zb.moment=wb),vb}),ub(!0)):ub()}).call(this); -------------------------------------------------------------------------------- /public/js/lib/Chart.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Chart.js 3 | * http://chartjs.org/ 4 | * Version: 1.0.1 5 | * 6 | * Copyright 2015 Nick Downie 7 | * Released under the MIT license 8 | * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md 9 | */ 10 | 11 | 12 | (function(){ 13 | 14 | "use strict"; 15 | 16 | //Declare root variable - window in the browser, global on the server 17 | var root = this, 18 | previous = root.Chart; 19 | 20 | //Occupy the global variable of Chart, and create a simple base class 21 | var Chart = function(context){ 22 | var chart = this; 23 | this.canvas = context.canvas; 24 | 25 | this.ctx = context; 26 | 27 | //Variables global to the chart 28 | var computeDimension = function(element,dimension) 29 | { 30 | if (element['offset'+dimension]) 31 | { 32 | return element['offset'+dimension]; 33 | } 34 | else 35 | { 36 | return document.defaultView.getComputedStyle(element).getPropertyValue(dimension); 37 | } 38 | } 39 | 40 | var width = this.width = computeDimension(context.canvas,'Width'); 41 | var height = this.height = computeDimension(context.canvas,'Height'); 42 | 43 | // Firefox requires this to work correctly 44 | context.canvas.width = width; 45 | context.canvas.height = height; 46 | 47 | this.aspectRatio = this.width / this.height; 48 | //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. 49 | helpers.retinaScale(this); 50 | 51 | return this; 52 | }; 53 | //Globally expose the defaults to allow for user updating/changing 54 | Chart.defaults = { 55 | global: { 56 | // Boolean - Whether to animate the chart 57 | animation: true, 58 | 59 | // Number - Number of animation steps 60 | animationSteps: 60, 61 | 62 | // String - Animation easing effect 63 | animationEasing: "easeOutQuart", 64 | 65 | // Boolean - If we should show the scale at all 66 | showScale: true, 67 | 68 | // Boolean - If we want to override with a hard coded scale 69 | scaleOverride: false, 70 | 71 | // ** Required if scaleOverride is true ** 72 | // Number - The number of steps in a hard coded scale 73 | scaleSteps: null, 74 | // Number - The value jump in the hard coded scale 75 | scaleStepWidth: null, 76 | // Number - The scale starting value 77 | scaleStartValue: null, 78 | 79 | // String - Colour of the scale line 80 | scaleLineColor: "rgba(0,0,0,.1)", 81 | 82 | // Number - Pixel width of the scale line 83 | scaleLineWidth: 1, 84 | 85 | // Boolean - Whether to show labels on the scale 86 | scaleShowLabels: true, 87 | 88 | // Interpolated JS string - can access value 89 | scaleLabel: "<%=value%>", 90 | 91 | // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there 92 | scaleIntegersOnly: true, 93 | 94 | // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value 95 | scaleBeginAtZero: false, 96 | 97 | // String - Scale label font declaration for the scale label 98 | scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", 99 | 100 | // Number - Scale label font size in pixels 101 | scaleFontSize: 12, 102 | 103 | // String - Scale label font weight style 104 | scaleFontStyle: "normal", 105 | 106 | // String - Scale label font colour 107 | scaleFontColor: "#666", 108 | 109 | // Boolean - whether or not the chart should be responsive and resize when the browser does. 110 | responsive: false, 111 | 112 | // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container 113 | maintainAspectRatio: true, 114 | 115 | // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove 116 | showTooltips: true, 117 | 118 | // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function 119 | customTooltips: false, 120 | 121 | // Array - Array of string names to attach tooltip events 122 | tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], 123 | 124 | // String - Tooltip background colour 125 | tooltipFillColor: "rgba(0,0,0,0.8)", 126 | 127 | // String - Tooltip label font declaration for the scale label 128 | tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", 129 | 130 | // Number - Tooltip label font size in pixels 131 | tooltipFontSize: 14, 132 | 133 | // String - Tooltip font weight style 134 | tooltipFontStyle: "normal", 135 | 136 | // String - Tooltip label font colour 137 | tooltipFontColor: "#fff", 138 | 139 | // String - Tooltip title font declaration for the scale label 140 | tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", 141 | 142 | // Number - Tooltip title font size in pixels 143 | tooltipTitleFontSize: 14, 144 | 145 | // String - Tooltip title font weight style 146 | tooltipTitleFontStyle: "bold", 147 | 148 | // String - Tooltip title font colour 149 | tooltipTitleFontColor: "#fff", 150 | 151 | // Number - pixel width of padding around tooltip text 152 | tooltipYPadding: 6, 153 | 154 | // Number - pixel width of padding around tooltip text 155 | tooltipXPadding: 6, 156 | 157 | // Number - Size of the caret on the tooltip 158 | tooltipCaretSize: 8, 159 | 160 | // Number - Pixel radius of the tooltip border 161 | tooltipCornerRadius: 6, 162 | 163 | // Number - Pixel offset from point x to tooltip edge 164 | tooltipXOffset: 10, 165 | 166 | // String - Template string for single tooltips 167 | tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", 168 | 169 | // String - Template string for single tooltips 170 | multiTooltipTemplate: "<%= value %>", 171 | 172 | // String - Colour behind the legend colour block 173 | multiTooltipKeyBackground: '#fff', 174 | 175 | // Function - Will fire on animation progression. 176 | onAnimationProgress: function(){}, 177 | 178 | // Function - Will fire on animation completion. 179 | onAnimationComplete: function(){} 180 | 181 | } 182 | }; 183 | 184 | //Create a dictionary of chart types, to allow for extension of existing types 185 | Chart.types = {}; 186 | 187 | //Global Chart helpers object for utility methods and classes 188 | var helpers = Chart.helpers = {}; 189 | 190 | //-- Basic js utility methods 191 | var each = helpers.each = function(loopable,callback,self){ 192 | var additionalArgs = Array.prototype.slice.call(arguments, 3); 193 | // Check to see if null or undefined firstly. 194 | if (loopable){ 195 | if (loopable.length === +loopable.length){ 196 | var i; 197 | for (i=0; i= 0; i--) { 269 | var currentItem = arrayToSearch[i]; 270 | if (filterCallback(currentItem)){ 271 | return currentItem; 272 | } 273 | } 274 | }, 275 | inherits = helpers.inherits = function(extensions){ 276 | //Basic javascript inheritance based on the model created in Backbone.js 277 | var parent = this; 278 | var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; 279 | 280 | var Surrogate = function(){ this.constructor = ChartElement;}; 281 | Surrogate.prototype = parent.prototype; 282 | ChartElement.prototype = new Surrogate(); 283 | 284 | ChartElement.extend = inherits; 285 | 286 | if (extensions) extend(ChartElement.prototype, extensions); 287 | 288 | ChartElement.__super__ = parent.prototype; 289 | 290 | return ChartElement; 291 | }, 292 | noop = helpers.noop = function(){}, 293 | uid = helpers.uid = (function(){ 294 | var id=0; 295 | return function(){ 296 | return "chart-" + id++; 297 | }; 298 | })(), 299 | warn = helpers.warn = function(str){ 300 | //Method for warning of errors 301 | if (window.console && typeof window.console.warn == "function") console.warn(str); 302 | }, 303 | amd = helpers.amd = (typeof define == 'function' && define.amd), 304 | //-- Math methods 305 | isNumber = helpers.isNumber = function(n){ 306 | return !isNaN(parseFloat(n)) && isFinite(n); 307 | }, 308 | max = helpers.max = function(array){ 309 | return Math.max.apply( Math, array ); 310 | }, 311 | min = helpers.min = function(array){ 312 | return Math.min.apply( Math, array ); 313 | }, 314 | cap = helpers.cap = function(valueToCap,maxValue,minValue){ 315 | if(isNumber(maxValue)) { 316 | if( valueToCap > maxValue ) { 317 | return maxValue; 318 | } 319 | } 320 | else if(isNumber(minValue)){ 321 | if ( valueToCap < minValue ){ 322 | return minValue; 323 | } 324 | } 325 | return valueToCap; 326 | }, 327 | getDecimalPlaces = helpers.getDecimalPlaces = function(num){ 328 | if (num%1!==0 && isNumber(num)){ 329 | return num.toString().split(".")[1].length; 330 | } 331 | else { 332 | return 0; 333 | } 334 | }, 335 | toRadians = helpers.radians = function(degrees){ 336 | return degrees * (Math.PI/180); 337 | }, 338 | // Gets the angle from vertical upright to the point about a centre. 339 | getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ 340 | var distanceFromXCenter = anglePoint.x - centrePoint.x, 341 | distanceFromYCenter = anglePoint.y - centrePoint.y, 342 | radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); 343 | 344 | 345 | var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); 346 | 347 | //If the segment is in the top left quadrant, we need to add another rotation to the angle 348 | if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ 349 | angle += Math.PI*2; 350 | } 351 | 352 | return { 353 | angle: angle, 354 | distance: radialDistanceFromCenter 355 | }; 356 | }, 357 | aliasPixel = helpers.aliasPixel = function(pixelWidth){ 358 | return (pixelWidth % 2 === 0) ? 0 : 0.5; 359 | }, 360 | splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ 361 | //Props to Rob Spencer at scaled innovation for his post on splining between points 362 | //http://scaledinnovation.com/analytics/splines/aboutSplines.html 363 | var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), 364 | d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), 365 | fa=t*d01/(d01+d12),// scaling factor for triangle Ta 366 | fb=t*d12/(d01+d12); 367 | return { 368 | inner : { 369 | x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), 370 | y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) 371 | }, 372 | outer : { 373 | x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), 374 | y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) 375 | } 376 | }; 377 | }, 378 | calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ 379 | return Math.floor(Math.log(val) / Math.LN10); 380 | }, 381 | calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ 382 | 383 | //Set a minimum step of two - a point at the top of the graph, and a point at the base 384 | var minSteps = 2, 385 | maxSteps = Math.floor(drawingSize/(textSize * 1.5)), 386 | skipFitting = (minSteps >= maxSteps); 387 | 388 | var maxValue = max(valuesArray), 389 | minValue = min(valuesArray); 390 | 391 | // We need some degree of seperation here to calculate the scales if all the values are the same 392 | // Adding/minusing 0.5 will give us a range of 1. 393 | if (maxValue === minValue){ 394 | maxValue += 0.5; 395 | // So we don't end up with a graph with a negative start value if we've said always start from zero 396 | if (minValue >= 0.5 && !startFromZero){ 397 | minValue -= 0.5; 398 | } 399 | else{ 400 | // Make up a whole number above the values 401 | maxValue += 0.5; 402 | } 403 | } 404 | 405 | var valueRange = Math.abs(maxValue - minValue), 406 | rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), 407 | graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), 408 | graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), 409 | graphRange = graphMax - graphMin, 410 | stepValue = Math.pow(10, rangeOrderOfMagnitude), 411 | numberOfSteps = Math.round(graphRange / stepValue); 412 | 413 | //If we have more space on the graph we'll use it to give more definition to the data 414 | while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { 415 | if(numberOfSteps > maxSteps){ 416 | stepValue *=2; 417 | numberOfSteps = Math.round(graphRange/stepValue); 418 | // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. 419 | if (numberOfSteps % 1 !== 0){ 420 | skipFitting = true; 421 | } 422 | } 423 | //We can fit in double the amount of scale points on the scale 424 | else{ 425 | //If user has declared ints only, and the step value isn't a decimal 426 | if (integersOnly && rangeOrderOfMagnitude >= 0){ 427 | //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float 428 | if(stepValue/2 % 1 === 0){ 429 | stepValue /=2; 430 | numberOfSteps = Math.round(graphRange/stepValue); 431 | } 432 | //If it would make it a float break out of the loop 433 | else{ 434 | break; 435 | } 436 | } 437 | //If the scale doesn't have to be an int, make the scale more granular anyway. 438 | else{ 439 | stepValue /=2; 440 | numberOfSteps = Math.round(graphRange/stepValue); 441 | } 442 | 443 | } 444 | } 445 | 446 | if (skipFitting){ 447 | numberOfSteps = minSteps; 448 | stepValue = graphRange / numberOfSteps; 449 | } 450 | 451 | return { 452 | steps : numberOfSteps, 453 | stepValue : stepValue, 454 | min : graphMin, 455 | max : graphMin + (numberOfSteps * stepValue) 456 | }; 457 | 458 | }, 459 | /* jshint ignore:start */ 460 | // Blows up jshint errors based on the new Function constructor 461 | //Templating methods 462 | //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ 463 | template = helpers.template = function(templateString, valuesObject){ 464 | 465 | // If templateString is function rather than string-template - call the function for valuesObject 466 | 467 | if(templateString instanceof Function){ 468 | return templateString(valuesObject); 469 | } 470 | 471 | var cache = {}; 472 | function tmpl(str, data){ 473 | // Figure out if we're getting a template, or if we need to 474 | // load the template - and be sure to cache the result. 475 | var fn = !/\W/.test(str) ? 476 | cache[str] = cache[str] : 477 | 478 | // Generate a reusable function that will serve as a template 479 | // generator (and which will be cached). 480 | new Function("obj", 481 | "var p=[],print=function(){p.push.apply(p,arguments);};" + 482 | 483 | // Introduce the data as local variables using with(){} 484 | "with(obj){p.push('" + 485 | 486 | // Convert the template into pure JavaScript 487 | str 488 | .replace(/[\r\t\n]/g, " ") 489 | .split("<%").join("\t") 490 | .replace(/((^|%>)[^\t]*)'/g, "$1\r") 491 | .replace(/\t=(.*?)%>/g, "',$1,'") 492 | .split("\t").join("');") 493 | .split("%>").join("p.push('") 494 | .split("\r").join("\\'") + 495 | "');}return p.join('');" 496 | ); 497 | 498 | // Provide some basic currying to the user 499 | return data ? fn( data ) : fn; 500 | } 501 | return tmpl(templateString,valuesObject); 502 | }, 503 | /* jshint ignore:end */ 504 | generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ 505 | var labelsArray = new Array(numberOfSteps); 506 | if (labelTemplateString){ 507 | each(labelsArray,function(val,index){ 508 | labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); 509 | }); 510 | } 511 | return labelsArray; 512 | }, 513 | //--Animation methods 514 | //Easing functions adapted from Robert Penner's easing equations 515 | //http://www.robertpenner.com/easing/ 516 | easingEffects = helpers.easingEffects = { 517 | linear: function (t) { 518 | return t; 519 | }, 520 | easeInQuad: function (t) { 521 | return t * t; 522 | }, 523 | easeOutQuad: function (t) { 524 | return -1 * t * (t - 2); 525 | }, 526 | easeInOutQuad: function (t) { 527 | if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; 528 | return -1 / 2 * ((--t) * (t - 2) - 1); 529 | }, 530 | easeInCubic: function (t) { 531 | return t * t * t; 532 | }, 533 | easeOutCubic: function (t) { 534 | return 1 * ((t = t / 1 - 1) * t * t + 1); 535 | }, 536 | easeInOutCubic: function (t) { 537 | if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t; 538 | return 1 / 2 * ((t -= 2) * t * t + 2); 539 | }, 540 | easeInQuart: function (t) { 541 | return t * t * t * t; 542 | }, 543 | easeOutQuart: function (t) { 544 | return -1 * ((t = t / 1 - 1) * t * t * t - 1); 545 | }, 546 | easeInOutQuart: function (t) { 547 | if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t; 548 | return -1 / 2 * ((t -= 2) * t * t * t - 2); 549 | }, 550 | easeInQuint: function (t) { 551 | return 1 * (t /= 1) * t * t * t * t; 552 | }, 553 | easeOutQuint: function (t) { 554 | return 1 * ((t = t / 1 - 1) * t * t * t * t + 1); 555 | }, 556 | easeInOutQuint: function (t) { 557 | if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t; 558 | return 1 / 2 * ((t -= 2) * t * t * t * t + 2); 559 | }, 560 | easeInSine: function (t) { 561 | return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; 562 | }, 563 | easeOutSine: function (t) { 564 | return 1 * Math.sin(t / 1 * (Math.PI / 2)); 565 | }, 566 | easeInOutSine: function (t) { 567 | return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); 568 | }, 569 | easeInExpo: function (t) { 570 | return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); 571 | }, 572 | easeOutExpo: function (t) { 573 | return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); 574 | }, 575 | easeInOutExpo: function (t) { 576 | if (t === 0) return 0; 577 | if (t === 1) return 1; 578 | if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1)); 579 | return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); 580 | }, 581 | easeInCirc: function (t) { 582 | if (t >= 1) return t; 583 | return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); 584 | }, 585 | easeOutCirc: function (t) { 586 | return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); 587 | }, 588 | easeInOutCirc: function (t) { 589 | if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1); 590 | return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); 591 | }, 592 | easeInElastic: function (t) { 593 | var s = 1.70158; 594 | var p = 0; 595 | var a = 1; 596 | if (t === 0) return 0; 597 | if ((t /= 1) == 1) return 1; 598 | if (!p) p = 1 * 0.3; 599 | if (a < Math.abs(1)) { 600 | a = 1; 601 | s = p / 4; 602 | } else s = p / (2 * Math.PI) * Math.asin(1 / a); 603 | return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); 604 | }, 605 | easeOutElastic: function (t) { 606 | var s = 1.70158; 607 | var p = 0; 608 | var a = 1; 609 | if (t === 0) return 0; 610 | if ((t /= 1) == 1) return 1; 611 | if (!p) p = 1 * 0.3; 612 | if (a < Math.abs(1)) { 613 | a = 1; 614 | s = p / 4; 615 | } else s = p / (2 * Math.PI) * Math.asin(1 / a); 616 | return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; 617 | }, 618 | easeInOutElastic: function (t) { 619 | var s = 1.70158; 620 | var p = 0; 621 | var a = 1; 622 | if (t === 0) return 0; 623 | if ((t /= 1 / 2) == 2) return 1; 624 | if (!p) p = 1 * (0.3 * 1.5); 625 | if (a < Math.abs(1)) { 626 | a = 1; 627 | s = p / 4; 628 | } else s = p / (2 * Math.PI) * Math.asin(1 / a); 629 | if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); 630 | return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; 631 | }, 632 | easeInBack: function (t) { 633 | var s = 1.70158; 634 | return 1 * (t /= 1) * t * ((s + 1) * t - s); 635 | }, 636 | easeOutBack: function (t) { 637 | var s = 1.70158; 638 | return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); 639 | }, 640 | easeInOutBack: function (t) { 641 | var s = 1.70158; 642 | if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); 643 | return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); 644 | }, 645 | easeInBounce: function (t) { 646 | return 1 - easingEffects.easeOutBounce(1 - t); 647 | }, 648 | easeOutBounce: function (t) { 649 | if ((t /= 1) < (1 / 2.75)) { 650 | return 1 * (7.5625 * t * t); 651 | } else if (t < (2 / 2.75)) { 652 | return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); 653 | } else if (t < (2.5 / 2.75)) { 654 | return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); 655 | } else { 656 | return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); 657 | } 658 | }, 659 | easeInOutBounce: function (t) { 660 | if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5; 661 | return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; 662 | } 663 | }, 664 | //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ 665 | requestAnimFrame = helpers.requestAnimFrame = (function(){ 666 | return window.requestAnimationFrame || 667 | window.webkitRequestAnimationFrame || 668 | window.mozRequestAnimationFrame || 669 | window.oRequestAnimationFrame || 670 | window.msRequestAnimationFrame || 671 | function(callback) { 672 | return window.setTimeout(callback, 1000 / 60); 673 | }; 674 | })(), 675 | cancelAnimFrame = helpers.cancelAnimFrame = (function(){ 676 | return window.cancelAnimationFrame || 677 | window.webkitCancelAnimationFrame || 678 | window.mozCancelAnimationFrame || 679 | window.oCancelAnimationFrame || 680 | window.msCancelAnimationFrame || 681 | function(callback) { 682 | return window.clearTimeout(callback, 1000 / 60); 683 | }; 684 | })(), 685 | animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ 686 | 687 | var currentStep = 0, 688 | easingFunction = easingEffects[easingString] || easingEffects.linear; 689 | 690 | var animationFrame = function(){ 691 | currentStep++; 692 | var stepDecimal = currentStep/totalSteps; 693 | var easeDecimal = easingFunction(stepDecimal); 694 | 695 | callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); 696 | onProgress.call(chartInstance,easeDecimal,stepDecimal); 697 | if (currentStep < totalSteps){ 698 | chartInstance.animationFrame = requestAnimFrame(animationFrame); 699 | } else{ 700 | onComplete.apply(chartInstance); 701 | } 702 | }; 703 | requestAnimFrame(animationFrame); 704 | }, 705 | //-- DOM methods 706 | getRelativePosition = helpers.getRelativePosition = function(evt){ 707 | var mouseX, mouseY; 708 | var e = evt.originalEvent || evt, 709 | canvas = evt.currentTarget || evt.srcElement, 710 | boundingRect = canvas.getBoundingClientRect(); 711 | 712 | if (e.touches){ 713 | mouseX = e.touches[0].clientX - boundingRect.left; 714 | mouseY = e.touches[0].clientY - boundingRect.top; 715 | 716 | } 717 | else{ 718 | mouseX = e.clientX - boundingRect.left; 719 | mouseY = e.clientY - boundingRect.top; 720 | } 721 | 722 | return { 723 | x : mouseX, 724 | y : mouseY 725 | }; 726 | 727 | }, 728 | addEvent = helpers.addEvent = function(node,eventType,method){ 729 | if (node.addEventListener){ 730 | node.addEventListener(eventType,method); 731 | } else if (node.attachEvent){ 732 | node.attachEvent("on"+eventType, method); 733 | } else { 734 | node["on"+eventType] = method; 735 | } 736 | }, 737 | removeEvent = helpers.removeEvent = function(node, eventType, handler){ 738 | if (node.removeEventListener){ 739 | node.removeEventListener(eventType, handler, false); 740 | } else if (node.detachEvent){ 741 | node.detachEvent("on"+eventType,handler); 742 | } else{ 743 | node["on" + eventType] = noop; 744 | } 745 | }, 746 | bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ 747 | // Create the events object if it's not already present 748 | if (!chartInstance.events) chartInstance.events = {}; 749 | 750 | each(arrayOfEvents,function(eventName){ 751 | chartInstance.events[eventName] = function(){ 752 | handler.apply(chartInstance, arguments); 753 | }; 754 | addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); 755 | }); 756 | }, 757 | unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { 758 | each(arrayOfEvents, function(handler,eventName){ 759 | removeEvent(chartInstance.chart.canvas, eventName, handler); 760 | }); 761 | }, 762 | getMaximumWidth = helpers.getMaximumWidth = function(domNode){ 763 | var container = domNode.parentNode; 764 | // TODO = check cross browser stuff with this. 765 | return container.clientWidth; 766 | }, 767 | getMaximumHeight = helpers.getMaximumHeight = function(domNode){ 768 | var container = domNode.parentNode; 769 | // TODO = check cross browser stuff with this. 770 | return container.clientHeight; 771 | }, 772 | getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support 773 | retinaScale = helpers.retinaScale = function(chart){ 774 | var ctx = chart.ctx, 775 | width = chart.canvas.width, 776 | height = chart.canvas.height; 777 | 778 | if (window.devicePixelRatio) { 779 | ctx.canvas.style.width = width + "px"; 780 | ctx.canvas.style.height = height + "px"; 781 | ctx.canvas.height = height * window.devicePixelRatio; 782 | ctx.canvas.width = width * window.devicePixelRatio; 783 | ctx.scale(window.devicePixelRatio, window.devicePixelRatio); 784 | } 785 | }, 786 | //-- Canvas methods 787 | clear = helpers.clear = function(chart){ 788 | chart.ctx.clearRect(0,0,chart.width,chart.height); 789 | }, 790 | fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ 791 | return fontStyle + " " + pixelSize+"px " + fontFamily; 792 | }, 793 | longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ 794 | ctx.font = font; 795 | var longest = 0; 796 | each(arrayOfStrings,function(string){ 797 | var textWidth = ctx.measureText(string).width; 798 | longest = (textWidth > longest) ? textWidth : longest; 799 | }); 800 | return longest; 801 | }, 802 | drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ 803 | ctx.beginPath(); 804 | ctx.moveTo(x + radius, y); 805 | ctx.lineTo(x + width - radius, y); 806 | ctx.quadraticCurveTo(x + width, y, x + width, y + radius); 807 | ctx.lineTo(x + width, y + height - radius); 808 | ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); 809 | ctx.lineTo(x + radius, y + height); 810 | ctx.quadraticCurveTo(x, y + height, x, y + height - radius); 811 | ctx.lineTo(x, y + radius); 812 | ctx.quadraticCurveTo(x, y, x + radius, y); 813 | ctx.closePath(); 814 | }; 815 | 816 | 817 | //Store a reference to each instance - allowing us to globally resize chart instances on window resize. 818 | //Destroy method on the chart will remove the instance of the chart from this reference. 819 | Chart.instances = {}; 820 | 821 | Chart.Type = function(data,options,chart){ 822 | this.options = options; 823 | this.chart = chart; 824 | this.id = uid(); 825 | //Add the chart instance to the global namespace 826 | Chart.instances[this.id] = this; 827 | 828 | // Initialize is always called when a chart type is created 829 | // By default it is a no op, but it should be extended 830 | if (options.responsive){ 831 | this.resize(); 832 | } 833 | this.initialize.call(this,data); 834 | }; 835 | 836 | //Core methods that'll be a part of every chart type 837 | extend(Chart.Type.prototype,{ 838 | initialize : function(){return this;}, 839 | clear : function(){ 840 | clear(this.chart); 841 | return this; 842 | }, 843 | stop : function(){ 844 | // Stops any current animation loop occuring 845 | helpers.cancelAnimFrame.call(root, this.animationFrame); 846 | return this; 847 | }, 848 | resize : function(callback){ 849 | this.stop(); 850 | var canvas = this.chart.canvas, 851 | newWidth = getMaximumWidth(this.chart.canvas), 852 | newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); 853 | 854 | canvas.width = this.chart.width = newWidth; 855 | canvas.height = this.chart.height = newHeight; 856 | 857 | retinaScale(this.chart); 858 | 859 | if (typeof callback === "function"){ 860 | callback.apply(this, Array.prototype.slice.call(arguments, 1)); 861 | } 862 | return this; 863 | }, 864 | reflow : noop, 865 | render : function(reflow){ 866 | if (reflow){ 867 | this.reflow(); 868 | } 869 | if (this.options.animation && !reflow){ 870 | helpers.animationLoop( 871 | this.draw, 872 | this.options.animationSteps, 873 | this.options.animationEasing, 874 | this.options.onAnimationProgress, 875 | this.options.onAnimationComplete, 876 | this 877 | ); 878 | } 879 | else{ 880 | this.draw(); 881 | this.options.onAnimationComplete.call(this); 882 | } 883 | return this; 884 | }, 885 | generateLegend : function(){ 886 | return template(this.options.legendTemplate,this); 887 | }, 888 | destroy : function(){ 889 | this.clear(); 890 | unbindEvents(this, this.events); 891 | var canvas = this.chart.canvas; 892 | 893 | // Reset canvas height/width attributes starts a fresh with the canvas context 894 | canvas.width = this.chart.width; 895 | canvas.height = this.chart.height; 896 | 897 | // < IE9 doesn't support removeProperty 898 | if (canvas.style.removeProperty) { 899 | canvas.style.removeProperty('width'); 900 | canvas.style.removeProperty('height'); 901 | } else { 902 | canvas.style.removeAttribute('width'); 903 | canvas.style.removeAttribute('height'); 904 | } 905 | 906 | delete Chart.instances[this.id]; 907 | }, 908 | showTooltip : function(ChartElements, forceRedraw){ 909 | // Only redraw the chart if we've actually changed what we're hovering on. 910 | if (typeof this.activeElements === 'undefined') this.activeElements = []; 911 | 912 | var isChanged = (function(Elements){ 913 | var changed = false; 914 | 915 | if (Elements.length !== this.activeElements.length){ 916 | changed = true; 917 | return changed; 918 | } 919 | 920 | each(Elements, function(element, index){ 921 | if (element !== this.activeElements[index]){ 922 | changed = true; 923 | } 924 | }, this); 925 | return changed; 926 | }).call(this, ChartElements); 927 | 928 | if (!isChanged && !forceRedraw){ 929 | return; 930 | } 931 | else{ 932 | this.activeElements = ChartElements; 933 | } 934 | this.draw(); 935 | if(this.options.customTooltips){ 936 | this.options.customTooltips(false); 937 | } 938 | if (ChartElements.length > 0){ 939 | // If we have multiple datasets, show a MultiTooltip for all of the data points at that index 940 | if (this.datasets && this.datasets.length > 1) { 941 | var dataArray, 942 | dataIndex; 943 | 944 | for (var i = this.datasets.length - 1; i >= 0; i--) { 945 | dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; 946 | dataIndex = indexOf(dataArray, ChartElements[0]); 947 | if (dataIndex !== -1){ 948 | break; 949 | } 950 | } 951 | var tooltipLabels = [], 952 | tooltipColors = [], 953 | medianPosition = (function(index) { 954 | 955 | // Get all the points at that particular index 956 | var Elements = [], 957 | dataCollection, 958 | xPositions = [], 959 | yPositions = [], 960 | xMax, 961 | yMax, 962 | xMin, 963 | yMin; 964 | helpers.each(this.datasets, function(dataset){ 965 | dataCollection = dataset.points || dataset.bars || dataset.segments; 966 | if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ 967 | Elements.push(dataCollection[dataIndex]); 968 | } 969 | }); 970 | 971 | helpers.each(Elements, function(element) { 972 | xPositions.push(element.x); 973 | yPositions.push(element.y); 974 | 975 | 976 | //Include any colour information about the element 977 | tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); 978 | tooltipColors.push({ 979 | fill: element._saved.fillColor || element.fillColor, 980 | stroke: element._saved.strokeColor || element.strokeColor 981 | }); 982 | 983 | }, this); 984 | 985 | yMin = min(yPositions); 986 | yMax = max(yPositions); 987 | 988 | xMin = min(xPositions); 989 | xMax = max(xPositions); 990 | 991 | return { 992 | x: (xMin > this.chart.width/2) ? xMin : xMax, 993 | y: (yMin + yMax)/2 994 | }; 995 | }).call(this, dataIndex); 996 | 997 | new Chart.MultiTooltip({ 998 | x: medianPosition.x, 999 | y: medianPosition.y, 1000 | xPadding: this.options.tooltipXPadding, 1001 | yPadding: this.options.tooltipYPadding, 1002 | xOffset: this.options.tooltipXOffset, 1003 | fillColor: this.options.tooltipFillColor, 1004 | textColor: this.options.tooltipFontColor, 1005 | fontFamily: this.options.tooltipFontFamily, 1006 | fontStyle: this.options.tooltipFontStyle, 1007 | fontSize: this.options.tooltipFontSize, 1008 | titleTextColor: this.options.tooltipTitleFontColor, 1009 | titleFontFamily: this.options.tooltipTitleFontFamily, 1010 | titleFontStyle: this.options.tooltipTitleFontStyle, 1011 | titleFontSize: this.options.tooltipTitleFontSize, 1012 | cornerRadius: this.options.tooltipCornerRadius, 1013 | labels: tooltipLabels, 1014 | legendColors: tooltipColors, 1015 | legendColorBackground : this.options.multiTooltipKeyBackground, 1016 | title: ChartElements[0].label, 1017 | chart: this.chart, 1018 | ctx: this.chart.ctx, 1019 | custom: this.options.customTooltips 1020 | }).draw(); 1021 | 1022 | } else { 1023 | each(ChartElements, function(Element) { 1024 | var tooltipPosition = Element.tooltipPosition(); 1025 | new Chart.Tooltip({ 1026 | x: Math.round(tooltipPosition.x), 1027 | y: Math.round(tooltipPosition.y), 1028 | xPadding: this.options.tooltipXPadding, 1029 | yPadding: this.options.tooltipYPadding, 1030 | fillColor: this.options.tooltipFillColor, 1031 | textColor: this.options.tooltipFontColor, 1032 | fontFamily: this.options.tooltipFontFamily, 1033 | fontStyle: this.options.tooltipFontStyle, 1034 | fontSize: this.options.tooltipFontSize, 1035 | caretHeight: this.options.tooltipCaretSize, 1036 | cornerRadius: this.options.tooltipCornerRadius, 1037 | text: template(this.options.tooltipTemplate, Element), 1038 | chart: this.chart, 1039 | custom: this.options.customTooltips 1040 | }).draw(); 1041 | }, this); 1042 | } 1043 | } 1044 | return this; 1045 | }, 1046 | toBase64Image : function(){ 1047 | return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); 1048 | } 1049 | }); 1050 | 1051 | Chart.Type.extend = function(extensions){ 1052 | 1053 | var parent = this; 1054 | 1055 | var ChartType = function(){ 1056 | return parent.apply(this,arguments); 1057 | }; 1058 | 1059 | //Copy the prototype object of the this class 1060 | ChartType.prototype = clone(parent.prototype); 1061 | //Now overwrite some of the properties in the base class with the new extensions 1062 | extend(ChartType.prototype, extensions); 1063 | 1064 | ChartType.extend = Chart.Type.extend; 1065 | 1066 | if (extensions.name || parent.prototype.name){ 1067 | 1068 | var chartName = extensions.name || parent.prototype.name; 1069 | //Assign any potential default values of the new chart type 1070 | 1071 | //If none are defined, we'll use a clone of the chart type this is being extended from. 1072 | //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart 1073 | //doesn't define some defaults of their own. 1074 | 1075 | var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; 1076 | 1077 | Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); 1078 | 1079 | Chart.types[chartName] = ChartType; 1080 | 1081 | //Register this new chart type in the Chart prototype 1082 | Chart.prototype[chartName] = function(data,options){ 1083 | var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); 1084 | return new ChartType(data,config,this); 1085 | }; 1086 | } else{ 1087 | warn("Name not provided for this chart, so it hasn't been registered"); 1088 | } 1089 | return parent; 1090 | }; 1091 | 1092 | Chart.Element = function(configuration){ 1093 | extend(this,configuration); 1094 | this.initialize.apply(this,arguments); 1095 | this.save(); 1096 | }; 1097 | extend(Chart.Element.prototype,{ 1098 | initialize : function(){}, 1099 | restore : function(props){ 1100 | if (!props){ 1101 | extend(this,this._saved); 1102 | } else { 1103 | each(props,function(key){ 1104 | this[key] = this._saved[key]; 1105 | },this); 1106 | } 1107 | return this; 1108 | }, 1109 | save : function(){ 1110 | this._saved = clone(this); 1111 | delete this._saved._saved; 1112 | return this; 1113 | }, 1114 | update : function(newProps){ 1115 | each(newProps,function(value,key){ 1116 | this._saved[key] = this[key]; 1117 | this[key] = value; 1118 | },this); 1119 | return this; 1120 | }, 1121 | transition : function(props,ease){ 1122 | each(props,function(value,key){ 1123 | this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; 1124 | },this); 1125 | return this; 1126 | }, 1127 | tooltipPosition : function(){ 1128 | return { 1129 | x : this.x, 1130 | y : this.y 1131 | }; 1132 | }, 1133 | hasValue: function(){ 1134 | return isNumber(this.value); 1135 | } 1136 | }); 1137 | 1138 | Chart.Element.extend = inherits; 1139 | 1140 | 1141 | Chart.Point = Chart.Element.extend({ 1142 | display: true, 1143 | inRange: function(chartX,chartY){ 1144 | var hitDetectionRange = this.hitDetectionRadius + this.radius; 1145 | return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); 1146 | }, 1147 | draw : function(){ 1148 | if (this.display){ 1149 | var ctx = this.ctx; 1150 | ctx.beginPath(); 1151 | 1152 | ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); 1153 | ctx.closePath(); 1154 | 1155 | ctx.strokeStyle = this.strokeColor; 1156 | ctx.lineWidth = this.strokeWidth; 1157 | 1158 | ctx.fillStyle = this.fillColor; 1159 | 1160 | ctx.fill(); 1161 | ctx.stroke(); 1162 | } 1163 | 1164 | 1165 | //Quick debug for bezier curve splining 1166 | //Highlights control points and the line between them. 1167 | //Handy for dev - stripped in the min version. 1168 | 1169 | // ctx.save(); 1170 | // ctx.fillStyle = "black"; 1171 | // ctx.strokeStyle = "black" 1172 | // ctx.beginPath(); 1173 | // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); 1174 | // ctx.fill(); 1175 | 1176 | // ctx.beginPath(); 1177 | // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); 1178 | // ctx.fill(); 1179 | 1180 | // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); 1181 | // ctx.lineTo(this.x, this.y); 1182 | // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); 1183 | // ctx.stroke(); 1184 | 1185 | // ctx.restore(); 1186 | 1187 | 1188 | 1189 | } 1190 | }); 1191 | 1192 | Chart.Arc = Chart.Element.extend({ 1193 | inRange : function(chartX,chartY){ 1194 | 1195 | var pointRelativePosition = helpers.getAngleFromPoint(this, { 1196 | x: chartX, 1197 | y: chartY 1198 | }); 1199 | 1200 | //Check if within the range of the open/close angle 1201 | var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), 1202 | withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); 1203 | 1204 | return (betweenAngles && withinRadius); 1205 | //Ensure within the outside of the arc centre, but inside arc outer 1206 | }, 1207 | tooltipPosition : function(){ 1208 | var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), 1209 | rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; 1210 | return { 1211 | x : this.x + (Math.cos(centreAngle) * rangeFromCentre), 1212 | y : this.y + (Math.sin(centreAngle) * rangeFromCentre) 1213 | }; 1214 | }, 1215 | draw : function(animationPercent){ 1216 | 1217 | var easingDecimal = animationPercent || 1; 1218 | 1219 | var ctx = this.ctx; 1220 | 1221 | ctx.beginPath(); 1222 | 1223 | ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); 1224 | 1225 | ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); 1226 | 1227 | ctx.closePath(); 1228 | ctx.strokeStyle = this.strokeColor; 1229 | ctx.lineWidth = this.strokeWidth; 1230 | 1231 | ctx.fillStyle = this.fillColor; 1232 | 1233 | ctx.fill(); 1234 | ctx.lineJoin = 'bevel'; 1235 | 1236 | if (this.showStroke){ 1237 | ctx.stroke(); 1238 | } 1239 | } 1240 | }); 1241 | 1242 | Chart.Rectangle = Chart.Element.extend({ 1243 | draw : function(){ 1244 | var ctx = this.ctx, 1245 | halfWidth = this.width/2, 1246 | leftX = this.x - halfWidth, 1247 | rightX = this.x + halfWidth, 1248 | top = this.base - (this.base - this.y), 1249 | halfStroke = this.strokeWidth / 2; 1250 | 1251 | // Canvas doesn't allow us to stroke inside the width so we can 1252 | // adjust the sizes to fit if we're setting a stroke on the line 1253 | if (this.showStroke){ 1254 | leftX += halfStroke; 1255 | rightX -= halfStroke; 1256 | top += halfStroke; 1257 | } 1258 | 1259 | ctx.beginPath(); 1260 | 1261 | ctx.fillStyle = this.fillColor; 1262 | ctx.strokeStyle = this.strokeColor; 1263 | ctx.lineWidth = this.strokeWidth; 1264 | 1265 | // It'd be nice to keep this class totally generic to any rectangle 1266 | // and simply specify which border to miss out. 1267 | ctx.moveTo(leftX, this.base); 1268 | ctx.lineTo(leftX, top); 1269 | ctx.lineTo(rightX, top); 1270 | ctx.lineTo(rightX, this.base); 1271 | ctx.fill(); 1272 | if (this.showStroke){ 1273 | ctx.stroke(); 1274 | } 1275 | }, 1276 | height : function(){ 1277 | return this.base - this.y; 1278 | }, 1279 | inRange : function(chartX,chartY){ 1280 | return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); 1281 | } 1282 | }); 1283 | 1284 | Chart.Tooltip = Chart.Element.extend({ 1285 | draw : function(){ 1286 | 1287 | var ctx = this.chart.ctx; 1288 | 1289 | ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); 1290 | 1291 | this.xAlign = "center"; 1292 | this.yAlign = "above"; 1293 | 1294 | //Distance between the actual element.y position and the start of the tooltip caret 1295 | var caretPadding = this.caretPadding = 2; 1296 | 1297 | var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, 1298 | tooltipRectHeight = this.fontSize + 2*this.yPadding, 1299 | tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; 1300 | 1301 | if (this.x + tooltipWidth/2 >this.chart.width){ 1302 | this.xAlign = "left"; 1303 | } else if (this.x - tooltipWidth/2 < 0){ 1304 | this.xAlign = "right"; 1305 | } 1306 | 1307 | if (this.y - tooltipHeight < 0){ 1308 | this.yAlign = "below"; 1309 | } 1310 | 1311 | 1312 | var tooltipX = this.x - tooltipWidth/2, 1313 | tooltipY = this.y - tooltipHeight; 1314 | 1315 | ctx.fillStyle = this.fillColor; 1316 | 1317 | // Custom Tooltips 1318 | if(this.custom){ 1319 | this.custom(this); 1320 | } 1321 | else{ 1322 | switch(this.yAlign) 1323 | { 1324 | case "above": 1325 | //Draw a caret above the x/y 1326 | ctx.beginPath(); 1327 | ctx.moveTo(this.x,this.y - caretPadding); 1328 | ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); 1329 | ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); 1330 | ctx.closePath(); 1331 | ctx.fill(); 1332 | break; 1333 | case "below": 1334 | tooltipY = this.y + caretPadding + this.caretHeight; 1335 | //Draw a caret below the x/y 1336 | ctx.beginPath(); 1337 | ctx.moveTo(this.x, this.y + caretPadding); 1338 | ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); 1339 | ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); 1340 | ctx.closePath(); 1341 | ctx.fill(); 1342 | break; 1343 | } 1344 | 1345 | switch(this.xAlign) 1346 | { 1347 | case "left": 1348 | tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); 1349 | break; 1350 | case "right": 1351 | tooltipX = this.x - (this.cornerRadius + this.caretHeight); 1352 | break; 1353 | } 1354 | 1355 | drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); 1356 | 1357 | ctx.fill(); 1358 | 1359 | ctx.fillStyle = this.textColor; 1360 | ctx.textAlign = "center"; 1361 | ctx.textBaseline = "middle"; 1362 | ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); 1363 | } 1364 | } 1365 | }); 1366 | 1367 | Chart.MultiTooltip = Chart.Element.extend({ 1368 | initialize : function(){ 1369 | this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); 1370 | 1371 | this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); 1372 | 1373 | this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; 1374 | 1375 | this.ctx.font = this.titleFont; 1376 | 1377 | var titleWidth = this.ctx.measureText(this.title).width, 1378 | //Label has a legend square as well so account for this. 1379 | labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, 1380 | longestTextWidth = max([labelWidth,titleWidth]); 1381 | 1382 | this.width = longestTextWidth + (this.xPadding*2); 1383 | 1384 | 1385 | var halfHeight = this.height/2; 1386 | 1387 | //Check to ensure the height will fit on the canvas 1388 | //The three is to buffer form the very 1389 | if (this.y - halfHeight < 0 ){ 1390 | this.y = halfHeight; 1391 | } else if (this.y + halfHeight > this.chart.height){ 1392 | this.y = this.chart.height - halfHeight; 1393 | } 1394 | 1395 | //Decide whether to align left or right based on position on canvas 1396 | if (this.x > this.chart.width/2){ 1397 | this.x -= this.xOffset + this.width; 1398 | } else { 1399 | this.x += this.xOffset; 1400 | } 1401 | 1402 | 1403 | }, 1404 | getLineHeight : function(index){ 1405 | var baseLineHeight = this.y - (this.height/2) + this.yPadding, 1406 | afterTitleIndex = index-1; 1407 | 1408 | //If the index is zero, we're getting the title 1409 | if (index === 0){ 1410 | return baseLineHeight + this.titleFontSize/2; 1411 | } else{ 1412 | return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; 1413 | } 1414 | 1415 | }, 1416 | draw : function(){ 1417 | // Custom Tooltips 1418 | if(this.custom){ 1419 | this.custom(this); 1420 | } 1421 | else{ 1422 | drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); 1423 | var ctx = this.ctx; 1424 | ctx.fillStyle = this.fillColor; 1425 | ctx.fill(); 1426 | ctx.closePath(); 1427 | 1428 | ctx.textAlign = "left"; 1429 | ctx.textBaseline = "middle"; 1430 | ctx.fillStyle = this.titleTextColor; 1431 | ctx.font = this.titleFont; 1432 | 1433 | ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); 1434 | 1435 | ctx.font = this.font; 1436 | helpers.each(this.labels,function(label,index){ 1437 | ctx.fillStyle = this.textColor; 1438 | ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); 1439 | 1440 | //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) 1441 | //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); 1442 | //Instead we'll make a white filled block to put the legendColour palette over. 1443 | 1444 | ctx.fillStyle = this.legendColorBackground; 1445 | ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); 1446 | 1447 | ctx.fillStyle = this.legendColors[index].fill; 1448 | ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); 1449 | 1450 | 1451 | },this); 1452 | } 1453 | } 1454 | }); 1455 | 1456 | Chart.Scale = Chart.Element.extend({ 1457 | initialize : function(){ 1458 | this.fit(); 1459 | }, 1460 | buildYLabels : function(){ 1461 | this.yLabels = []; 1462 | 1463 | var stepDecimalPlaces = getDecimalPlaces(this.stepValue); 1464 | 1465 | for (var i=0; i<=this.steps; i++){ 1466 | this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); 1467 | } 1468 | this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0; 1469 | }, 1470 | addXLabel : function(label){ 1471 | this.xLabels.push(label); 1472 | this.valuesCount++; 1473 | this.fit(); 1474 | }, 1475 | removeXLabel : function(){ 1476 | this.xLabels.shift(); 1477 | this.valuesCount--; 1478 | this.fit(); 1479 | }, 1480 | // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use 1481 | fit: function(){ 1482 | // First we need the width of the yLabels, assuming the xLabels aren't rotated 1483 | 1484 | // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation 1485 | this.startPoint = (this.display) ? this.fontSize : 0; 1486 | this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels 1487 | 1488 | // Apply padding settings to the start and end point. 1489 | this.startPoint += this.padding; 1490 | this.endPoint -= this.padding; 1491 | 1492 | // Cache the starting height, so can determine if we need to recalculate the scale yAxis 1493 | var cachedHeight = this.endPoint - this.startPoint, 1494 | cachedYLabelWidth; 1495 | 1496 | // Build the current yLabels so we have an idea of what size they'll be to start 1497 | /* 1498 | * This sets what is returned from calculateScaleRange as static properties of this class: 1499 | * 1500 | this.steps; 1501 | this.stepValue; 1502 | this.min; 1503 | this.max; 1504 | * 1505 | */ 1506 | this.calculateYRange(cachedHeight); 1507 | 1508 | // With these properties set we can now build the array of yLabels 1509 | // and also the width of the largest yLabel 1510 | this.buildYLabels(); 1511 | 1512 | this.calculateXLabelRotation(); 1513 | 1514 | while((cachedHeight > this.endPoint - this.startPoint)){ 1515 | cachedHeight = this.endPoint - this.startPoint; 1516 | cachedYLabelWidth = this.yLabelWidth; 1517 | 1518 | this.calculateYRange(cachedHeight); 1519 | this.buildYLabels(); 1520 | 1521 | // Only go through the xLabel loop again if the yLabel width has changed 1522 | if (cachedYLabelWidth < this.yLabelWidth){ 1523 | this.calculateXLabelRotation(); 1524 | } 1525 | } 1526 | 1527 | }, 1528 | calculateXLabelRotation : function(){ 1529 | //Get the width of each grid by calculating the difference 1530 | //between x offsets between 0 and 1. 1531 | 1532 | this.ctx.font = this.font; 1533 | 1534 | var firstWidth = this.ctx.measureText(this.xLabels[0]).width, 1535 | lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, 1536 | firstRotated, 1537 | lastRotated; 1538 | 1539 | 1540 | this.xScalePaddingRight = lastWidth/2 + 3; 1541 | this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; 1542 | 1543 | this.xLabelRotation = 0; 1544 | if (this.display){ 1545 | var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), 1546 | cosRotation, 1547 | firstRotatedWidth; 1548 | this.xLabelWidth = originalLabelWidth; 1549 | //Allow 3 pixels x2 padding either side for label readability 1550 | var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; 1551 | 1552 | //Max label rotate should be 90 - also act as a loop counter 1553 | while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ 1554 | cosRotation = Math.cos(toRadians(this.xLabelRotation)); 1555 | 1556 | firstRotated = cosRotation * firstWidth; 1557 | lastRotated = cosRotation * lastWidth; 1558 | 1559 | // We're right aligning the text now. 1560 | if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ 1561 | this.xScalePaddingLeft = firstRotated + this.fontSize / 2; 1562 | } 1563 | this.xScalePaddingRight = this.fontSize/2; 1564 | 1565 | 1566 | this.xLabelRotation++; 1567 | this.xLabelWidth = cosRotation * originalLabelWidth; 1568 | 1569 | } 1570 | if (this.xLabelRotation > 0){ 1571 | this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; 1572 | } 1573 | } 1574 | else{ 1575 | this.xLabelWidth = 0; 1576 | this.xScalePaddingRight = this.padding; 1577 | this.xScalePaddingLeft = this.padding; 1578 | } 1579 | 1580 | }, 1581 | // Needs to be overidden in each Chart type 1582 | // Otherwise we need to pass all the data into the scale class 1583 | calculateYRange: noop, 1584 | drawingArea: function(){ 1585 | return this.startPoint - this.endPoint; 1586 | }, 1587 | calculateY : function(value){ 1588 | var scalingFactor = this.drawingArea() / (this.min - this.max); 1589 | return this.endPoint - (scalingFactor * (value - this.min)); 1590 | }, 1591 | calculateX : function(index){ 1592 | var isRotated = (this.xLabelRotation > 0), 1593 | // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, 1594 | innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), 1595 | valueWidth = innerWidth/(this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1596 | valueOffset = (valueWidth * index) + this.xScalePaddingLeft; 1597 | 1598 | if (this.offsetGridLines){ 1599 | valueOffset += (valueWidth/2); 1600 | } 1601 | 1602 | return Math.round(valueOffset); 1603 | }, 1604 | update : function(newProps){ 1605 | helpers.extend(this, newProps); 1606 | this.fit(); 1607 | }, 1608 | draw : function(){ 1609 | var ctx = this.ctx, 1610 | yLabelGap = (this.endPoint - this.startPoint) / this.steps, 1611 | xStart = Math.round(this.xScalePaddingLeft); 1612 | if (this.display){ 1613 | ctx.fillStyle = this.textColor; 1614 | ctx.font = this.font; 1615 | each(this.yLabels,function(labelString,index){ 1616 | var yLabelCenter = this.endPoint - (yLabelGap * index), 1617 | linePositionY = Math.round(yLabelCenter), 1618 | drawHorizontalLine = this.showHorizontalLines; 1619 | 1620 | ctx.textAlign = "right"; 1621 | ctx.textBaseline = "middle"; 1622 | if (this.showLabels){ 1623 | ctx.fillText(labelString,xStart - 10,yLabelCenter); 1624 | } 1625 | 1626 | // This is X axis, so draw it 1627 | if (index === 0 && !drawHorizontalLine){ 1628 | drawHorizontalLine = true; 1629 | } 1630 | 1631 | if (drawHorizontalLine){ 1632 | ctx.beginPath(); 1633 | } 1634 | 1635 | if (index > 0){ 1636 | // This is a grid line in the centre, so drop that 1637 | ctx.lineWidth = this.gridLineWidth; 1638 | ctx.strokeStyle = this.gridLineColor; 1639 | } else { 1640 | // This is the first line on the scale 1641 | ctx.lineWidth = this.lineWidth; 1642 | ctx.strokeStyle = this.lineColor; 1643 | } 1644 | 1645 | linePositionY += helpers.aliasPixel(ctx.lineWidth); 1646 | 1647 | if(drawHorizontalLine){ 1648 | ctx.moveTo(xStart, linePositionY); 1649 | ctx.lineTo(this.width, linePositionY); 1650 | ctx.stroke(); 1651 | ctx.closePath(); 1652 | } 1653 | 1654 | ctx.lineWidth = this.lineWidth; 1655 | ctx.strokeStyle = this.lineColor; 1656 | ctx.beginPath(); 1657 | ctx.moveTo(xStart - 5, linePositionY); 1658 | ctx.lineTo(xStart, linePositionY); 1659 | ctx.stroke(); 1660 | ctx.closePath(); 1661 | 1662 | },this); 1663 | 1664 | each(this.xLabels,function(label,index){ 1665 | var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), 1666 | // Check to see if line/bar here and decide where to place the line 1667 | linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), 1668 | isRotated = (this.xLabelRotation > 0), 1669 | drawVerticalLine = this.showVerticalLines; 1670 | 1671 | // This is Y axis, so draw it 1672 | if (index === 0 && !drawVerticalLine){ 1673 | drawVerticalLine = true; 1674 | } 1675 | 1676 | if (drawVerticalLine){ 1677 | ctx.beginPath(); 1678 | } 1679 | 1680 | if (index > 0){ 1681 | // This is a grid line in the centre, so drop that 1682 | ctx.lineWidth = this.gridLineWidth; 1683 | ctx.strokeStyle = this.gridLineColor; 1684 | } else { 1685 | // This is the first line on the scale 1686 | ctx.lineWidth = this.lineWidth; 1687 | ctx.strokeStyle = this.lineColor; 1688 | } 1689 | 1690 | if (drawVerticalLine){ 1691 | ctx.moveTo(linePos,this.endPoint); 1692 | ctx.lineTo(linePos,this.startPoint - 3); 1693 | ctx.stroke(); 1694 | ctx.closePath(); 1695 | } 1696 | 1697 | 1698 | ctx.lineWidth = this.lineWidth; 1699 | ctx.strokeStyle = this.lineColor; 1700 | 1701 | 1702 | // Small lines at the bottom of the base grid line 1703 | ctx.beginPath(); 1704 | ctx.moveTo(linePos,this.endPoint); 1705 | ctx.lineTo(linePos,this.endPoint + 5); 1706 | ctx.stroke(); 1707 | ctx.closePath(); 1708 | 1709 | ctx.save(); 1710 | ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); 1711 | ctx.rotate(toRadians(this.xLabelRotation)*-1); 1712 | ctx.font = this.font; 1713 | ctx.textAlign = (isRotated) ? "right" : "center"; 1714 | ctx.textBaseline = (isRotated) ? "middle" : "top"; 1715 | ctx.fillText(label, 0, 0); 1716 | ctx.restore(); 1717 | },this); 1718 | 1719 | } 1720 | } 1721 | 1722 | }); 1723 | 1724 | Chart.RadialScale = Chart.Element.extend({ 1725 | initialize: function(){ 1726 | this.size = min([this.height, this.width]); 1727 | this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); 1728 | }, 1729 | calculateCenterOffset: function(value){ 1730 | // Take into account half font size + the yPadding of the top value 1731 | var scalingFactor = this.drawingArea / (this.max - this.min); 1732 | 1733 | return (value - this.min) * scalingFactor; 1734 | }, 1735 | update : function(){ 1736 | if (!this.lineArc){ 1737 | this.setScaleSize(); 1738 | } else { 1739 | this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); 1740 | } 1741 | this.buildYLabels(); 1742 | }, 1743 | buildYLabels: function(){ 1744 | this.yLabels = []; 1745 | 1746 | var stepDecimalPlaces = getDecimalPlaces(this.stepValue); 1747 | 1748 | for (var i=0; i<=this.steps; i++){ 1749 | this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); 1750 | } 1751 | }, 1752 | getCircumference : function(){ 1753 | return ((Math.PI*2) / this.valuesCount); 1754 | }, 1755 | setScaleSize: function(){ 1756 | /* 1757 | * Right, this is really confusing and there is a lot of maths going on here 1758 | * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 1759 | * 1760 | * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif 1761 | * 1762 | * Solution: 1763 | * 1764 | * We assume the radius of the polygon is half the size of the canvas at first 1765 | * at each index we check if the text overlaps. 1766 | * 1767 | * Where it does, we store that angle and that index. 1768 | * 1769 | * After finding the largest index and angle we calculate how much we need to remove 1770 | * from the shape radius to move the point inwards by that x. 1771 | * 1772 | * We average the left and right distances to get the maximum shape radius that can fit in the box 1773 | * along with labels. 1774 | * 1775 | * Once we have that, we can find the centre point for the chart, by taking the x text protrusion 1776 | * on each side, removing that from the size, halving it and adding the left x protrusion width. 1777 | * 1778 | * This will mean we have a shape fitted to the canvas, as large as it can be with the labels 1779 | * and position it in the most space efficient manner 1780 | * 1781 | * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif 1782 | */ 1783 | 1784 | 1785 | // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. 1786 | // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points 1787 | var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), 1788 | pointPosition, 1789 | i, 1790 | textWidth, 1791 | halfTextWidth, 1792 | furthestRight = this.width, 1793 | furthestRightIndex, 1794 | furthestRightAngle, 1795 | furthestLeft = 0, 1796 | furthestLeftIndex, 1797 | furthestLeftAngle, 1798 | xProtrusionLeft, 1799 | xProtrusionRight, 1800 | radiusReductionRight, 1801 | radiusReductionLeft, 1802 | maxWidthRadius; 1803 | this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); 1804 | for (i=0;i furthestRight) { 1814 | furthestRight = pointPosition.x + halfTextWidth; 1815 | furthestRightIndex = i; 1816 | } 1817 | if (pointPosition.x - halfTextWidth < furthestLeft) { 1818 | furthestLeft = pointPosition.x - halfTextWidth; 1819 | furthestLeftIndex = i; 1820 | } 1821 | } 1822 | else if (i < this.valuesCount/2) { 1823 | // Less than half the values means we'll left align the text 1824 | if (pointPosition.x + textWidth > furthestRight) { 1825 | furthestRight = pointPosition.x + textWidth; 1826 | furthestRightIndex = i; 1827 | } 1828 | } 1829 | else if (i > this.valuesCount/2){ 1830 | // More than half the values means we'll right align the text 1831 | if (pointPosition.x - textWidth < furthestLeft) { 1832 | furthestLeft = pointPosition.x - textWidth; 1833 | furthestLeftIndex = i; 1834 | } 1835 | } 1836 | } 1837 | 1838 | xProtrusionLeft = furthestLeft; 1839 | 1840 | xProtrusionRight = Math.ceil(furthestRight - this.width); 1841 | 1842 | furthestRightAngle = this.getIndexAngle(furthestRightIndex); 1843 | 1844 | furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); 1845 | 1846 | radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); 1847 | 1848 | radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); 1849 | 1850 | // Ensure we actually need to reduce the size of the chart 1851 | radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; 1852 | radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; 1853 | 1854 | this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; 1855 | 1856 | //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) 1857 | this.setCenterPoint(radiusReductionLeft, radiusReductionRight); 1858 | 1859 | }, 1860 | setCenterPoint: function(leftMovement, rightMovement){ 1861 | 1862 | var maxRight = this.width - rightMovement - this.drawingArea, 1863 | maxLeft = leftMovement + this.drawingArea; 1864 | 1865 | this.xCenter = (maxLeft + maxRight)/2; 1866 | // Always vertically in the centre as the text height doesn't change 1867 | this.yCenter = (this.height/2); 1868 | }, 1869 | 1870 | getIndexAngle : function(index){ 1871 | var angleMultiplier = (Math.PI * 2) / this.valuesCount; 1872 | // Start from the top instead of right, so remove a quarter of the circle 1873 | 1874 | return index * angleMultiplier - (Math.PI/2); 1875 | }, 1876 | getPointPosition : function(index, distanceFromCenter){ 1877 | var thisAngle = this.getIndexAngle(index); 1878 | return { 1879 | x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, 1880 | y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter 1881 | }; 1882 | }, 1883 | draw: function(){ 1884 | if (this.display){ 1885 | var ctx = this.ctx; 1886 | each(this.yLabels, function(label, index){ 1887 | // Don't draw a centre value 1888 | if (index > 0){ 1889 | var yCenterOffset = index * (this.drawingArea/this.steps), 1890 | yHeight = this.yCenter - yCenterOffset, 1891 | pointPosition; 1892 | 1893 | // Draw circular lines around the scale 1894 | if (this.lineWidth > 0){ 1895 | ctx.strokeStyle = this.lineColor; 1896 | ctx.lineWidth = this.lineWidth; 1897 | 1898 | if(this.lineArc){ 1899 | ctx.beginPath(); 1900 | ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); 1901 | ctx.closePath(); 1902 | ctx.stroke(); 1903 | } else{ 1904 | ctx.beginPath(); 1905 | for (var i=0;i= 0; i--) { 1942 | if (this.angleLineWidth > 0){ 1943 | var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); 1944 | ctx.beginPath(); 1945 | ctx.moveTo(this.xCenter, this.yCenter); 1946 | ctx.lineTo(outerPosition.x, outerPosition.y); 1947 | ctx.stroke(); 1948 | ctx.closePath(); 1949 | } 1950 | // Extra 3px out for some label spacing 1951 | var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); 1952 | ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); 1953 | ctx.fillStyle = this.pointLabelFontColor; 1954 | 1955 | var labelsCount = this.labels.length, 1956 | halfLabelsCount = this.labels.length/2, 1957 | quarterLabelsCount = halfLabelsCount/2, 1958 | upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), 1959 | exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); 1960 | if (i === 0){ 1961 | ctx.textAlign = 'center'; 1962 | } else if(i === halfLabelsCount){ 1963 | ctx.textAlign = 'center'; 1964 | } else if (i < halfLabelsCount){ 1965 | ctx.textAlign = 'left'; 1966 | } else { 1967 | ctx.textAlign = 'right'; 1968 | } 1969 | 1970 | // Set the correct text baseline based on outer positioning 1971 | if (exactQuarter){ 1972 | ctx.textBaseline = 'middle'; 1973 | } else if (upperHalf){ 1974 | ctx.textBaseline = 'bottom'; 1975 | } else { 1976 | ctx.textBaseline = 'top'; 1977 | } 1978 | 1979 | ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); 1980 | } 1981 | } 1982 | } 1983 | } 1984 | }); 1985 | 1986 | // Attach global event to resize each chart instance when the browser resizes 1987 | helpers.addEvent(window, "resize", (function(){ 1988 | // Basic debounce of resize function so it doesn't hurt performance when resizing browser. 1989 | var timeout; 1990 | return function(){ 1991 | clearTimeout(timeout); 1992 | timeout = setTimeout(function(){ 1993 | each(Chart.instances,function(instance){ 1994 | // If the responsive flag is set in the chart instance config 1995 | // Cascade the resize event down to the chart. 1996 | if (instance.options.responsive){ 1997 | instance.resize(instance.render, true); 1998 | } 1999 | }); 2000 | }, 50); 2001 | }; 2002 | })()); 2003 | 2004 | 2005 | if (amd) { 2006 | define(function(){ 2007 | return Chart; 2008 | }); 2009 | } else if (typeof module === 'object' && module.exports) { 2010 | module.exports = Chart; 2011 | } 2012 | 2013 | root.Chart = Chart; 2014 | 2015 | Chart.noConflict = function(){ 2016 | root.Chart = previous; 2017 | return Chart; 2018 | }; 2019 | 2020 | }).call(this); 2021 | 2022 | (function(){ 2023 | "use strict"; 2024 | 2025 | var root = this, 2026 | Chart = root.Chart, 2027 | helpers = Chart.helpers; 2028 | 2029 | 2030 | var defaultConfig = { 2031 | //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value 2032 | scaleBeginAtZero : true, 2033 | 2034 | //Boolean - Whether grid lines are shown across the chart 2035 | scaleShowGridLines : true, 2036 | 2037 | //String - Colour of the grid lines 2038 | scaleGridLineColor : "rgba(0,0,0,.05)", 2039 | 2040 | //Number - Width of the grid lines 2041 | scaleGridLineWidth : 1, 2042 | 2043 | //Boolean - Whether to show horizontal lines (except X axis) 2044 | scaleShowHorizontalLines: true, 2045 | 2046 | //Boolean - Whether to show vertical lines (except Y axis) 2047 | scaleShowVerticalLines: true, 2048 | 2049 | //Boolean - If there is a stroke on each bar 2050 | barShowStroke : true, 2051 | 2052 | //Number - Pixel width of the bar stroke 2053 | barStrokeWidth : 2, 2054 | 2055 | //Number - Spacing between each of the X value sets 2056 | barValueSpacing : 5, 2057 | 2058 | //Number - Spacing between data sets within X values 2059 | barDatasetSpacing : 1, 2060 | 2061 | //String - A legend template 2062 | legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" 2063 | 2064 | }; 2065 | 2066 | 2067 | Chart.Type.extend({ 2068 | name: "Bar", 2069 | defaults : defaultConfig, 2070 | initialize: function(data){ 2071 | 2072 | //Expose options as a scope variable here so we can access it in the ScaleClass 2073 | var options = this.options; 2074 | 2075 | this.ScaleClass = Chart.Scale.extend({ 2076 | offsetGridLines : true, 2077 | calculateBarX : function(datasetCount, datasetIndex, barIndex){ 2078 | //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar 2079 | var xWidth = this.calculateBaseWidth(), 2080 | xAbsolute = this.calculateX(barIndex) - (xWidth/2), 2081 | barWidth = this.calculateBarWidth(datasetCount); 2082 | 2083 | return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; 2084 | }, 2085 | calculateBaseWidth : function(){ 2086 | return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); 2087 | }, 2088 | calculateBarWidth : function(datasetCount){ 2089 | //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset 2090 | var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); 2091 | 2092 | return (baseWidth / datasetCount); 2093 | } 2094 | }); 2095 | 2096 | this.datasets = []; 2097 | 2098 | //Set up tooltip events on the chart 2099 | if (this.options.showTooltips){ 2100 | helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ 2101 | var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; 2102 | 2103 | this.eachBars(function(bar){ 2104 | bar.restore(['fillColor', 'strokeColor']); 2105 | }); 2106 | helpers.each(activeBars, function(activeBar){ 2107 | activeBar.fillColor = activeBar.highlightFill; 2108 | activeBar.strokeColor = activeBar.highlightStroke; 2109 | }); 2110 | this.showTooltip(activeBars); 2111 | }); 2112 | } 2113 | 2114 | //Declare the extension of the default point, to cater for the options passed in to the constructor 2115 | this.BarClass = Chart.Rectangle.extend({ 2116 | strokeWidth : this.options.barStrokeWidth, 2117 | showStroke : this.options.barShowStroke, 2118 | ctx : this.chart.ctx 2119 | }); 2120 | 2121 | //Iterate through each of the datasets, and build this into a property of the chart 2122 | helpers.each(data.datasets,function(dataset,datasetIndex){ 2123 | 2124 | var datasetObject = { 2125 | label : dataset.label || null, 2126 | fillColor : dataset.fillColor, 2127 | strokeColor : dataset.strokeColor, 2128 | bars : [] 2129 | }; 2130 | 2131 | this.datasets.push(datasetObject); 2132 | 2133 | helpers.each(dataset.data,function(dataPoint,index){ 2134 | //Add a new point for each piece of data, passing any required data to draw. 2135 | datasetObject.bars.push(new this.BarClass({ 2136 | value : dataPoint, 2137 | label : data.labels[index], 2138 | datasetLabel: dataset.label, 2139 | strokeColor : dataset.strokeColor, 2140 | fillColor : dataset.fillColor, 2141 | highlightFill : dataset.highlightFill || dataset.fillColor, 2142 | highlightStroke : dataset.highlightStroke || dataset.strokeColor 2143 | })); 2144 | },this); 2145 | 2146 | },this); 2147 | 2148 | this.buildScale(data.labels); 2149 | 2150 | this.BarClass.prototype.base = this.scale.endPoint; 2151 | 2152 | this.eachBars(function(bar, index, datasetIndex){ 2153 | helpers.extend(bar, { 2154 | width : this.scale.calculateBarWidth(this.datasets.length), 2155 | x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index), 2156 | y: this.scale.endPoint 2157 | }); 2158 | bar.save(); 2159 | }, this); 2160 | 2161 | this.render(); 2162 | }, 2163 | update : function(){ 2164 | this.scale.update(); 2165 | // Reset any highlight colours before updating. 2166 | helpers.each(this.activeElements, function(activeElement){ 2167 | activeElement.restore(['fillColor', 'strokeColor']); 2168 | }); 2169 | 2170 | this.eachBars(function(bar){ 2171 | bar.save(); 2172 | }); 2173 | this.render(); 2174 | }, 2175 | eachBars : function(callback){ 2176 | helpers.each(this.datasets,function(dataset, datasetIndex){ 2177 | helpers.each(dataset.bars, callback, this, datasetIndex); 2178 | },this); 2179 | }, 2180 | getBarsAtEvent : function(e){ 2181 | var barsArray = [], 2182 | eventPosition = helpers.getRelativePosition(e), 2183 | datasetIterator = function(dataset){ 2184 | barsArray.push(dataset.bars[barIndex]); 2185 | }, 2186 | barIndex; 2187 | 2188 | for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { 2189 | for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { 2190 | if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ 2191 | helpers.each(this.datasets, datasetIterator); 2192 | return barsArray; 2193 | } 2194 | } 2195 | } 2196 | 2197 | return barsArray; 2198 | }, 2199 | buildScale : function(labels){ 2200 | var self = this; 2201 | 2202 | var dataTotal = function(){ 2203 | var values = []; 2204 | self.eachBars(function(bar){ 2205 | values.push(bar.value); 2206 | }); 2207 | return values; 2208 | }; 2209 | 2210 | var scaleOptions = { 2211 | templateString : this.options.scaleLabel, 2212 | height : this.chart.height, 2213 | width : this.chart.width, 2214 | ctx : this.chart.ctx, 2215 | textColor : this.options.scaleFontColor, 2216 | fontSize : this.options.scaleFontSize, 2217 | fontStyle : this.options.scaleFontStyle, 2218 | fontFamily : this.options.scaleFontFamily, 2219 | valuesCount : labels.length, 2220 | beginAtZero : this.options.scaleBeginAtZero, 2221 | integersOnly : this.options.scaleIntegersOnly, 2222 | calculateYRange: function(currentHeight){ 2223 | var updatedRanges = helpers.calculateScaleRange( 2224 | dataTotal(), 2225 | currentHeight, 2226 | this.fontSize, 2227 | this.beginAtZero, 2228 | this.integersOnly 2229 | ); 2230 | helpers.extend(this, updatedRanges); 2231 | }, 2232 | xLabels : labels, 2233 | font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), 2234 | lineWidth : this.options.scaleLineWidth, 2235 | lineColor : this.options.scaleLineColor, 2236 | showHorizontalLines : this.options.scaleShowHorizontalLines, 2237 | showVerticalLines : this.options.scaleShowVerticalLines, 2238 | gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, 2239 | gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", 2240 | padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, 2241 | showLabels : this.options.scaleShowLabels, 2242 | display : this.options.showScale 2243 | }; 2244 | 2245 | if (this.options.scaleOverride){ 2246 | helpers.extend(scaleOptions, { 2247 | calculateYRange: helpers.noop, 2248 | steps: this.options.scaleSteps, 2249 | stepValue: this.options.scaleStepWidth, 2250 | min: this.options.scaleStartValue, 2251 | max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) 2252 | }); 2253 | } 2254 | 2255 | this.scale = new this.ScaleClass(scaleOptions); 2256 | }, 2257 | addData : function(valuesArray,label){ 2258 | //Map the values array for each of the datasets 2259 | helpers.each(valuesArray,function(value,datasetIndex){ 2260 | //Add a new point for each piece of data, passing any required data to draw. 2261 | this.datasets[datasetIndex].bars.push(new this.BarClass({ 2262 | value : value, 2263 | label : label, 2264 | x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), 2265 | y: this.scale.endPoint, 2266 | width : this.scale.calculateBarWidth(this.datasets.length), 2267 | base : this.scale.endPoint, 2268 | strokeColor : this.datasets[datasetIndex].strokeColor, 2269 | fillColor : this.datasets[datasetIndex].fillColor 2270 | })); 2271 | },this); 2272 | 2273 | this.scale.addXLabel(label); 2274 | //Then re-render the chart. 2275 | this.update(); 2276 | }, 2277 | removeData : function(){ 2278 | this.scale.removeXLabel(); 2279 | //Then re-render the chart. 2280 | helpers.each(this.datasets,function(dataset){ 2281 | dataset.bars.shift(); 2282 | },this); 2283 | this.update(); 2284 | }, 2285 | reflow : function(){ 2286 | helpers.extend(this.BarClass.prototype,{ 2287 | y: this.scale.endPoint, 2288 | base : this.scale.endPoint 2289 | }); 2290 | var newScaleProps = helpers.extend({ 2291 | height : this.chart.height, 2292 | width : this.chart.width 2293 | }); 2294 | this.scale.update(newScaleProps); 2295 | }, 2296 | draw : function(ease){ 2297 | var easingDecimal = ease || 1; 2298 | this.clear(); 2299 | 2300 | var ctx = this.chart.ctx; 2301 | 2302 | this.scale.draw(easingDecimal); 2303 | 2304 | //Draw all the bars for each dataset 2305 | helpers.each(this.datasets,function(dataset,datasetIndex){ 2306 | helpers.each(dataset.bars,function(bar,index){ 2307 | if (bar.hasValue()){ 2308 | bar.base = this.scale.endPoint; 2309 | //Transition then draw 2310 | bar.transition({ 2311 | x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), 2312 | y : this.scale.calculateY(bar.value), 2313 | width : this.scale.calculateBarWidth(this.datasets.length) 2314 | }, easingDecimal).draw(); 2315 | } 2316 | },this); 2317 | 2318 | },this); 2319 | } 2320 | }); 2321 | 2322 | 2323 | }).call(this); 2324 | 2325 | (function(){ 2326 | "use strict"; 2327 | 2328 | var root = this, 2329 | Chart = root.Chart, 2330 | //Cache a local reference to Chart.helpers 2331 | helpers = Chart.helpers; 2332 | 2333 | var defaultConfig = { 2334 | //Boolean - Whether we should show a stroke on each segment 2335 | segmentShowStroke : true, 2336 | 2337 | //String - The colour of each segment stroke 2338 | segmentStrokeColor : "#fff", 2339 | 2340 | //Number - The width of each segment stroke 2341 | segmentStrokeWidth : 2, 2342 | 2343 | //The percentage of the chart that we cut out of the middle. 2344 | percentageInnerCutout : 50, 2345 | 2346 | //Number - Amount of animation steps 2347 | animationSteps : 100, 2348 | 2349 | //String - Animation easing effect 2350 | animationEasing : "easeOutBounce", 2351 | 2352 | //Boolean - Whether we animate the rotation of the Doughnut 2353 | animateRotate : true, 2354 | 2355 | //Boolean - Whether we animate scaling the Doughnut from the centre 2356 | animateScale : false, 2357 | 2358 | //String - A legend template 2359 | legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" 2360 | 2361 | }; 2362 | 2363 | 2364 | Chart.Type.extend({ 2365 | //Passing in a name registers this chart in the Chart namespace 2366 | name: "Doughnut", 2367 | //Providing a defaults will also register the deafults in the chart namespace 2368 | defaults : defaultConfig, 2369 | //Initialize is fired when the chart is initialized - Data is passed in as a parameter 2370 | //Config is automatically merged by the core of Chart.js, and is available at this.options 2371 | initialize: function(data){ 2372 | 2373 | //Declare segments as a static property to prevent inheriting across the Chart type prototype 2374 | this.segments = []; 2375 | this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; 2376 | 2377 | this.SegmentArc = Chart.Arc.extend({ 2378 | ctx : this.chart.ctx, 2379 | x : this.chart.width/2, 2380 | y : this.chart.height/2 2381 | }); 2382 | 2383 | //Set up tooltip events on the chart 2384 | if (this.options.showTooltips){ 2385 | helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ 2386 | var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; 2387 | 2388 | helpers.each(this.segments,function(segment){ 2389 | segment.restore(["fillColor"]); 2390 | }); 2391 | helpers.each(activeSegments,function(activeSegment){ 2392 | activeSegment.fillColor = activeSegment.highlightColor; 2393 | }); 2394 | this.showTooltip(activeSegments); 2395 | }); 2396 | } 2397 | this.calculateTotal(data); 2398 | 2399 | helpers.each(data,function(datapoint, index){ 2400 | this.addData(datapoint, index, true); 2401 | },this); 2402 | 2403 | this.render(); 2404 | }, 2405 | getSegmentsAtEvent : function(e){ 2406 | var segmentsArray = []; 2407 | 2408 | var location = helpers.getRelativePosition(e); 2409 | 2410 | helpers.each(this.segments,function(segment){ 2411 | if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); 2412 | },this); 2413 | return segmentsArray; 2414 | }, 2415 | addData : function(segment, atIndex, silent){ 2416 | var index = atIndex || this.segments.length; 2417 | this.segments.splice(index, 0, new this.SegmentArc({ 2418 | value : segment.value, 2419 | outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, 2420 | innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, 2421 | fillColor : segment.color, 2422 | highlightColor : segment.highlight || segment.color, 2423 | showStroke : this.options.segmentShowStroke, 2424 | strokeWidth : this.options.segmentStrokeWidth, 2425 | strokeColor : this.options.segmentStrokeColor, 2426 | startAngle : Math.PI * 1.5, 2427 | circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), 2428 | label : segment.label 2429 | })); 2430 | if (!silent){ 2431 | this.reflow(); 2432 | this.update(); 2433 | } 2434 | }, 2435 | calculateCircumference : function(value){ 2436 | return (Math.PI*2)*(value / this.total); 2437 | }, 2438 | calculateTotal : function(data){ 2439 | this.total = 0; 2440 | helpers.each(data,function(segment){ 2441 | this.total += segment.value; 2442 | },this); 2443 | }, 2444 | update : function(){ 2445 | this.calculateTotal(this.segments); 2446 | 2447 | // Reset any highlight colours before updating. 2448 | helpers.each(this.activeElements, function(activeElement){ 2449 | activeElement.restore(['fillColor']); 2450 | }); 2451 | 2452 | helpers.each(this.segments,function(segment){ 2453 | segment.save(); 2454 | }); 2455 | this.render(); 2456 | }, 2457 | 2458 | removeData: function(atIndex){ 2459 | var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; 2460 | this.segments.splice(indexToDelete, 1); 2461 | this.reflow(); 2462 | this.update(); 2463 | }, 2464 | 2465 | reflow : function(){ 2466 | helpers.extend(this.SegmentArc.prototype,{ 2467 | x : this.chart.width/2, 2468 | y : this.chart.height/2 2469 | }); 2470 | this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; 2471 | helpers.each(this.segments, function(segment){ 2472 | segment.update({ 2473 | outerRadius : this.outerRadius, 2474 | innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout 2475 | }); 2476 | }, this); 2477 | }, 2478 | draw : function(easeDecimal){ 2479 | var animDecimal = (easeDecimal) ? easeDecimal : 1; 2480 | this.clear(); 2481 | helpers.each(this.segments,function(segment,index){ 2482 | segment.transition({ 2483 | circumference : this.calculateCircumference(segment.value), 2484 | outerRadius : this.outerRadius, 2485 | innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout 2486 | },animDecimal); 2487 | 2488 | segment.endAngle = segment.startAngle + segment.circumference; 2489 | 2490 | segment.draw(); 2491 | if (index === 0){ 2492 | segment.startAngle = Math.PI * 1.5; 2493 | } 2494 | //Check to see if it's the last segment, if not get the next and update the start angle 2495 | if (index < this.segments.length-1){ 2496 | this.segments[index+1].startAngle = segment.endAngle; 2497 | } 2498 | },this); 2499 | 2500 | } 2501 | }); 2502 | 2503 | Chart.types.Doughnut.extend({ 2504 | name : "Pie", 2505 | defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) 2506 | }); 2507 | 2508 | }).call(this); 2509 | (function(){ 2510 | "use strict"; 2511 | 2512 | var root = this, 2513 | Chart = root.Chart, 2514 | helpers = Chart.helpers; 2515 | 2516 | var defaultConfig = { 2517 | 2518 | ///Boolean - Whether grid lines are shown across the chart 2519 | scaleShowGridLines : true, 2520 | 2521 | //String - Colour of the grid lines 2522 | scaleGridLineColor : "rgba(0,0,0,.05)", 2523 | 2524 | //Number - Width of the grid lines 2525 | scaleGridLineWidth : 1, 2526 | 2527 | //Boolean - Whether to show horizontal lines (except X axis) 2528 | scaleShowHorizontalLines: true, 2529 | 2530 | //Boolean - Whether to show vertical lines (except Y axis) 2531 | scaleShowVerticalLines: true, 2532 | 2533 | //Boolean - Whether the line is curved between points 2534 | bezierCurve : true, 2535 | 2536 | //Number - Tension of the bezier curve between points 2537 | bezierCurveTension : 0.4, 2538 | 2539 | //Boolean - Whether to show a dot for each point 2540 | pointDot : true, 2541 | 2542 | //Number - Radius of each point dot in pixels 2543 | pointDotRadius : 4, 2544 | 2545 | //Number - Pixel width of point dot stroke 2546 | pointDotStrokeWidth : 1, 2547 | 2548 | //Number - amount extra to add to the radius to cater for hit detection outside the drawn point 2549 | pointHitDetectionRadius : 20, 2550 | 2551 | //Boolean - Whether to show a stroke for datasets 2552 | datasetStroke : true, 2553 | 2554 | //Number - Pixel width of dataset stroke 2555 | datasetStrokeWidth : 2, 2556 | 2557 | //Boolean - Whether to fill the dataset with a colour 2558 | datasetFill : true, 2559 | 2560 | //String - A legend template 2561 | legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" 2562 | 2563 | }; 2564 | 2565 | 2566 | Chart.Type.extend({ 2567 | name: "Line", 2568 | defaults : defaultConfig, 2569 | initialize: function(data){ 2570 | //Declare the extension of the default point, to cater for the options passed in to the constructor 2571 | this.PointClass = Chart.Point.extend({ 2572 | strokeWidth : this.options.pointDotStrokeWidth, 2573 | radius : this.options.pointDotRadius, 2574 | display: this.options.pointDot, 2575 | hitDetectionRadius : this.options.pointHitDetectionRadius, 2576 | ctx : this.chart.ctx, 2577 | inRange : function(mouseX){ 2578 | return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); 2579 | } 2580 | }); 2581 | 2582 | this.datasets = []; 2583 | 2584 | //Set up tooltip events on the chart 2585 | if (this.options.showTooltips){ 2586 | helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ 2587 | var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; 2588 | this.eachPoints(function(point){ 2589 | point.restore(['fillColor', 'strokeColor']); 2590 | }); 2591 | helpers.each(activePoints, function(activePoint){ 2592 | activePoint.fillColor = activePoint.highlightFill; 2593 | activePoint.strokeColor = activePoint.highlightStroke; 2594 | }); 2595 | this.showTooltip(activePoints); 2596 | }); 2597 | } 2598 | 2599 | //Iterate through each of the datasets, and build this into a property of the chart 2600 | helpers.each(data.datasets,function(dataset){ 2601 | 2602 | var datasetObject = { 2603 | label : dataset.label || null, 2604 | fillColor : dataset.fillColor, 2605 | strokeColor : dataset.strokeColor, 2606 | pointColor : dataset.pointColor, 2607 | pointStrokeColor : dataset.pointStrokeColor, 2608 | points : [] 2609 | }; 2610 | 2611 | this.datasets.push(datasetObject); 2612 | 2613 | 2614 | helpers.each(dataset.data,function(dataPoint,index){ 2615 | //Add a new point for each piece of data, passing any required data to draw. 2616 | datasetObject.points.push(new this.PointClass({ 2617 | value : dataPoint, 2618 | label : data.labels[index], 2619 | datasetLabel: dataset.label, 2620 | strokeColor : dataset.pointStrokeColor, 2621 | fillColor : dataset.pointColor, 2622 | highlightFill : dataset.pointHighlightFill || dataset.pointColor, 2623 | highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor 2624 | })); 2625 | },this); 2626 | 2627 | this.buildScale(data.labels); 2628 | 2629 | 2630 | this.eachPoints(function(point, index){ 2631 | helpers.extend(point, { 2632 | x: this.scale.calculateX(index), 2633 | y: this.scale.endPoint 2634 | }); 2635 | point.save(); 2636 | }, this); 2637 | 2638 | },this); 2639 | 2640 | 2641 | this.render(); 2642 | }, 2643 | update : function(){ 2644 | this.scale.update(); 2645 | // Reset any highlight colours before updating. 2646 | helpers.each(this.activeElements, function(activeElement){ 2647 | activeElement.restore(['fillColor', 'strokeColor']); 2648 | }); 2649 | this.eachPoints(function(point){ 2650 | point.save(); 2651 | }); 2652 | this.render(); 2653 | }, 2654 | eachPoints : function(callback){ 2655 | helpers.each(this.datasets,function(dataset){ 2656 | helpers.each(dataset.points,callback,this); 2657 | },this); 2658 | }, 2659 | getPointsAtEvent : function(e){ 2660 | var pointsArray = [], 2661 | eventPosition = helpers.getRelativePosition(e); 2662 | helpers.each(this.datasets,function(dataset){ 2663 | helpers.each(dataset.points,function(point){ 2664 | if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); 2665 | }); 2666 | },this); 2667 | return pointsArray; 2668 | }, 2669 | buildScale : function(labels){ 2670 | var self = this; 2671 | 2672 | var dataTotal = function(){ 2673 | var values = []; 2674 | self.eachPoints(function(point){ 2675 | values.push(point.value); 2676 | }); 2677 | 2678 | return values; 2679 | }; 2680 | 2681 | var scaleOptions = { 2682 | templateString : this.options.scaleLabel, 2683 | height : this.chart.height, 2684 | width : this.chart.width, 2685 | ctx : this.chart.ctx, 2686 | textColor : this.options.scaleFontColor, 2687 | fontSize : this.options.scaleFontSize, 2688 | fontStyle : this.options.scaleFontStyle, 2689 | fontFamily : this.options.scaleFontFamily, 2690 | valuesCount : labels.length, 2691 | beginAtZero : this.options.scaleBeginAtZero, 2692 | integersOnly : this.options.scaleIntegersOnly, 2693 | calculateYRange : function(currentHeight){ 2694 | var updatedRanges = helpers.calculateScaleRange( 2695 | dataTotal(), 2696 | currentHeight, 2697 | this.fontSize, 2698 | this.beginAtZero, 2699 | this.integersOnly 2700 | ); 2701 | helpers.extend(this, updatedRanges); 2702 | }, 2703 | xLabels : labels, 2704 | font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), 2705 | lineWidth : this.options.scaleLineWidth, 2706 | lineColor : this.options.scaleLineColor, 2707 | showHorizontalLines : this.options.scaleShowHorizontalLines, 2708 | showVerticalLines : this.options.scaleShowVerticalLines, 2709 | gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, 2710 | gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", 2711 | padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, 2712 | showLabels : this.options.scaleShowLabels, 2713 | display : this.options.showScale 2714 | }; 2715 | 2716 | if (this.options.scaleOverride){ 2717 | helpers.extend(scaleOptions, { 2718 | calculateYRange: helpers.noop, 2719 | steps: this.options.scaleSteps, 2720 | stepValue: this.options.scaleStepWidth, 2721 | min: this.options.scaleStartValue, 2722 | max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) 2723 | }); 2724 | } 2725 | 2726 | 2727 | this.scale = new Chart.Scale(scaleOptions); 2728 | }, 2729 | addData : function(valuesArray,label){ 2730 | //Map the values array for each of the datasets 2731 | 2732 | helpers.each(valuesArray,function(value,datasetIndex){ 2733 | //Add a new point for each piece of data, passing any required data to draw. 2734 | this.datasets[datasetIndex].points.push(new this.PointClass({ 2735 | value : value, 2736 | label : label, 2737 | x: this.scale.calculateX(this.scale.valuesCount+1), 2738 | y: this.scale.endPoint, 2739 | strokeColor : this.datasets[datasetIndex].pointStrokeColor, 2740 | fillColor : this.datasets[datasetIndex].pointColor 2741 | })); 2742 | },this); 2743 | 2744 | this.scale.addXLabel(label); 2745 | //Then re-render the chart. 2746 | this.update(); 2747 | }, 2748 | removeData : function(){ 2749 | this.scale.removeXLabel(); 2750 | //Then re-render the chart. 2751 | helpers.each(this.datasets,function(dataset){ 2752 | dataset.points.shift(); 2753 | },this); 2754 | this.update(); 2755 | }, 2756 | reflow : function(){ 2757 | var newScaleProps = helpers.extend({ 2758 | height : this.chart.height, 2759 | width : this.chart.width 2760 | }); 2761 | this.scale.update(newScaleProps); 2762 | }, 2763 | draw : function(ease){ 2764 | var easingDecimal = ease || 1; 2765 | this.clear(); 2766 | 2767 | var ctx = this.chart.ctx; 2768 | 2769 | // Some helper methods for getting the next/prev points 2770 | var hasValue = function(item){ 2771 | return item.value !== null; 2772 | }, 2773 | nextPoint = function(point, collection, index){ 2774 | return helpers.findNextWhere(collection, hasValue, index) || point; 2775 | }, 2776 | previousPoint = function(point, collection, index){ 2777 | return helpers.findPreviousWhere(collection, hasValue, index) || point; 2778 | }; 2779 | 2780 | this.scale.draw(easingDecimal); 2781 | 2782 | 2783 | helpers.each(this.datasets,function(dataset){ 2784 | var pointsWithValues = helpers.where(dataset.points, hasValue); 2785 | 2786 | //Transition each point first so that the line and point drawing isn't out of sync 2787 | //We can use this extra loop to calculate the control points of this dataset also in this loop 2788 | 2789 | helpers.each(dataset.points, function(point, index){ 2790 | if (point.hasValue()){ 2791 | point.transition({ 2792 | y : this.scale.calculateY(point.value), 2793 | x : this.scale.calculateX(index) 2794 | }, easingDecimal); 2795 | } 2796 | },this); 2797 | 2798 | 2799 | // Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point 2800 | // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed 2801 | if (this.options.bezierCurve){ 2802 | helpers.each(pointsWithValues, function(point, index){ 2803 | var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; 2804 | point.controlPoints = helpers.splineCurve( 2805 | previousPoint(point, pointsWithValues, index), 2806 | point, 2807 | nextPoint(point, pointsWithValues, index), 2808 | tension 2809 | ); 2810 | 2811 | // Prevent the bezier going outside of the bounds of the graph 2812 | 2813 | // Cap puter bezier handles to the upper/lower scale bounds 2814 | if (point.controlPoints.outer.y > this.scale.endPoint){ 2815 | point.controlPoints.outer.y = this.scale.endPoint; 2816 | } 2817 | else if (point.controlPoints.outer.y < this.scale.startPoint){ 2818 | point.controlPoints.outer.y = this.scale.startPoint; 2819 | } 2820 | 2821 | // Cap inner bezier handles to the upper/lower scale bounds 2822 | if (point.controlPoints.inner.y > this.scale.endPoint){ 2823 | point.controlPoints.inner.y = this.scale.endPoint; 2824 | } 2825 | else if (point.controlPoints.inner.y < this.scale.startPoint){ 2826 | point.controlPoints.inner.y = this.scale.startPoint; 2827 | } 2828 | },this); 2829 | } 2830 | 2831 | 2832 | //Draw the line between all the points 2833 | ctx.lineWidth = this.options.datasetStrokeWidth; 2834 | ctx.strokeStyle = dataset.strokeColor; 2835 | ctx.beginPath(); 2836 | 2837 | helpers.each(pointsWithValues, function(point, index){ 2838 | if (index === 0){ 2839 | ctx.moveTo(point.x, point.y); 2840 | } 2841 | else{ 2842 | if(this.options.bezierCurve){ 2843 | var previous = previousPoint(point, pointsWithValues, index); 2844 | 2845 | ctx.bezierCurveTo( 2846 | previous.controlPoints.outer.x, 2847 | previous.controlPoints.outer.y, 2848 | point.controlPoints.inner.x, 2849 | point.controlPoints.inner.y, 2850 | point.x, 2851 | point.y 2852 | ); 2853 | } 2854 | else{ 2855 | ctx.lineTo(point.x,point.y); 2856 | } 2857 | } 2858 | }, this); 2859 | 2860 | ctx.stroke(); 2861 | 2862 | if (this.options.datasetFill && pointsWithValues.length > 0){ 2863 | //Round off the line by going to the base of the chart, back to the start, then fill. 2864 | ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); 2865 | ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint); 2866 | ctx.fillStyle = dataset.fillColor; 2867 | ctx.closePath(); 2868 | ctx.fill(); 2869 | } 2870 | 2871 | //Now draw the points over the line 2872 | //A little inefficient double looping, but better than the line 2873 | //lagging behind the point positions 2874 | helpers.each(pointsWithValues,function(point){ 2875 | point.draw(); 2876 | }); 2877 | },this); 2878 | } 2879 | }); 2880 | 2881 | 2882 | }).call(this); 2883 | 2884 | (function(){ 2885 | "use strict"; 2886 | 2887 | var root = this, 2888 | Chart = root.Chart, 2889 | //Cache a local reference to Chart.helpers 2890 | helpers = Chart.helpers; 2891 | 2892 | var defaultConfig = { 2893 | //Boolean - Show a backdrop to the scale label 2894 | scaleShowLabelBackdrop : true, 2895 | 2896 | //String - The colour of the label backdrop 2897 | scaleBackdropColor : "rgba(255,255,255,0.75)", 2898 | 2899 | // Boolean - Whether the scale should begin at zero 2900 | scaleBeginAtZero : true, 2901 | 2902 | //Number - The backdrop padding above & below the label in pixels 2903 | scaleBackdropPaddingY : 2, 2904 | 2905 | //Number - The backdrop padding to the side of the label in pixels 2906 | scaleBackdropPaddingX : 2, 2907 | 2908 | //Boolean - Show line for each value in the scale 2909 | scaleShowLine : true, 2910 | 2911 | //Boolean - Stroke a line around each segment in the chart 2912 | segmentShowStroke : true, 2913 | 2914 | //String - The colour of the stroke on each segement. 2915 | segmentStrokeColor : "#fff", 2916 | 2917 | //Number - The width of the stroke value in pixels 2918 | segmentStrokeWidth : 2, 2919 | 2920 | //Number - Amount of animation steps 2921 | animationSteps : 100, 2922 | 2923 | //String - Animation easing effect. 2924 | animationEasing : "easeOutBounce", 2925 | 2926 | //Boolean - Whether to animate the rotation of the chart 2927 | animateRotate : true, 2928 | 2929 | //Boolean - Whether to animate scaling the chart from the centre 2930 | animateScale : false, 2931 | 2932 | //String - A legend template 2933 | legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" 2934 | }; 2935 | 2936 | 2937 | Chart.Type.extend({ 2938 | //Passing in a name registers this chart in the Chart namespace 2939 | name: "PolarArea", 2940 | //Providing a defaults will also register the deafults in the chart namespace 2941 | defaults : defaultConfig, 2942 | //Initialize is fired when the chart is initialized - Data is passed in as a parameter 2943 | //Config is automatically merged by the core of Chart.js, and is available at this.options 2944 | initialize: function(data){ 2945 | this.segments = []; 2946 | //Declare segment class as a chart instance specific class, so it can share props for this instance 2947 | this.SegmentArc = Chart.Arc.extend({ 2948 | showStroke : this.options.segmentShowStroke, 2949 | strokeWidth : this.options.segmentStrokeWidth, 2950 | strokeColor : this.options.segmentStrokeColor, 2951 | ctx : this.chart.ctx, 2952 | innerRadius : 0, 2953 | x : this.chart.width/2, 2954 | y : this.chart.height/2 2955 | }); 2956 | this.scale = new Chart.RadialScale({ 2957 | display: this.options.showScale, 2958 | fontStyle: this.options.scaleFontStyle, 2959 | fontSize: this.options.scaleFontSize, 2960 | fontFamily: this.options.scaleFontFamily, 2961 | fontColor: this.options.scaleFontColor, 2962 | showLabels: this.options.scaleShowLabels, 2963 | showLabelBackdrop: this.options.scaleShowLabelBackdrop, 2964 | backdropColor: this.options.scaleBackdropColor, 2965 | backdropPaddingY : this.options.scaleBackdropPaddingY, 2966 | backdropPaddingX: this.options.scaleBackdropPaddingX, 2967 | lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, 2968 | lineColor: this.options.scaleLineColor, 2969 | lineArc: true, 2970 | width: this.chart.width, 2971 | height: this.chart.height, 2972 | xCenter: this.chart.width/2, 2973 | yCenter: this.chart.height/2, 2974 | ctx : this.chart.ctx, 2975 | templateString: this.options.scaleLabel, 2976 | valuesCount: data.length 2977 | }); 2978 | 2979 | this.updateScaleRange(data); 2980 | 2981 | this.scale.update(); 2982 | 2983 | helpers.each(data,function(segment,index){ 2984 | this.addData(segment,index,true); 2985 | },this); 2986 | 2987 | //Set up tooltip events on the chart 2988 | if (this.options.showTooltips){ 2989 | helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ 2990 | var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; 2991 | helpers.each(this.segments,function(segment){ 2992 | segment.restore(["fillColor"]); 2993 | }); 2994 | helpers.each(activeSegments,function(activeSegment){ 2995 | activeSegment.fillColor = activeSegment.highlightColor; 2996 | }); 2997 | this.showTooltip(activeSegments); 2998 | }); 2999 | } 3000 | 3001 | this.render(); 3002 | }, 3003 | getSegmentsAtEvent : function(e){ 3004 | var segmentsArray = []; 3005 | 3006 | var location = helpers.getRelativePosition(e); 3007 | 3008 | helpers.each(this.segments,function(segment){ 3009 | if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); 3010 | },this); 3011 | return segmentsArray; 3012 | }, 3013 | addData : function(segment, atIndex, silent){ 3014 | var index = atIndex || this.segments.length; 3015 | 3016 | this.segments.splice(index, 0, new this.SegmentArc({ 3017 | fillColor: segment.color, 3018 | highlightColor: segment.highlight || segment.color, 3019 | label: segment.label, 3020 | value: segment.value, 3021 | outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), 3022 | circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), 3023 | startAngle: Math.PI * 1.5 3024 | })); 3025 | if (!silent){ 3026 | this.reflow(); 3027 | this.update(); 3028 | } 3029 | }, 3030 | removeData: function(atIndex){ 3031 | var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; 3032 | this.segments.splice(indexToDelete, 1); 3033 | this.reflow(); 3034 | this.update(); 3035 | }, 3036 | calculateTotal: function(data){ 3037 | this.total = 0; 3038 | helpers.each(data,function(segment){ 3039 | this.total += segment.value; 3040 | },this); 3041 | this.scale.valuesCount = this.segments.length; 3042 | }, 3043 | updateScaleRange: function(datapoints){ 3044 | var valuesArray = []; 3045 | helpers.each(datapoints,function(segment){ 3046 | valuesArray.push(segment.value); 3047 | }); 3048 | 3049 | var scaleSizes = (this.options.scaleOverride) ? 3050 | { 3051 | steps: this.options.scaleSteps, 3052 | stepValue: this.options.scaleStepWidth, 3053 | min: this.options.scaleStartValue, 3054 | max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) 3055 | } : 3056 | helpers.calculateScaleRange( 3057 | valuesArray, 3058 | helpers.min([this.chart.width, this.chart.height])/2, 3059 | this.options.scaleFontSize, 3060 | this.options.scaleBeginAtZero, 3061 | this.options.scaleIntegersOnly 3062 | ); 3063 | 3064 | helpers.extend( 3065 | this.scale, 3066 | scaleSizes, 3067 | { 3068 | size: helpers.min([this.chart.width, this.chart.height]), 3069 | xCenter: this.chart.width/2, 3070 | yCenter: this.chart.height/2 3071 | } 3072 | ); 3073 | 3074 | }, 3075 | update : function(){ 3076 | this.calculateTotal(this.segments); 3077 | 3078 | helpers.each(this.segments,function(segment){ 3079 | segment.save(); 3080 | }); 3081 | this.render(); 3082 | }, 3083 | reflow : function(){ 3084 | helpers.extend(this.SegmentArc.prototype,{ 3085 | x : this.chart.width/2, 3086 | y : this.chart.height/2 3087 | }); 3088 | this.updateScaleRange(this.segments); 3089 | this.scale.update(); 3090 | 3091 | helpers.extend(this.scale,{ 3092 | xCenter: this.chart.width/2, 3093 | yCenter: this.chart.height/2 3094 | }); 3095 | 3096 | helpers.each(this.segments, function(segment){ 3097 | segment.update({ 3098 | outerRadius : this.scale.calculateCenterOffset(segment.value) 3099 | }); 3100 | }, this); 3101 | 3102 | }, 3103 | draw : function(ease){ 3104 | var easingDecimal = ease || 1; 3105 | //Clear & draw the canvas 3106 | this.clear(); 3107 | helpers.each(this.segments,function(segment, index){ 3108 | segment.transition({ 3109 | circumference : this.scale.getCircumference(), 3110 | outerRadius : this.scale.calculateCenterOffset(segment.value) 3111 | },easingDecimal); 3112 | 3113 | segment.endAngle = segment.startAngle + segment.circumference; 3114 | 3115 | // If we've removed the first segment we need to set the first one to 3116 | // start at the top. 3117 | if (index === 0){ 3118 | segment.startAngle = Math.PI * 1.5; 3119 | } 3120 | 3121 | //Check to see if it's the last segment, if not get the next and update the start angle 3122 | if (index < this.segments.length - 1){ 3123 | this.segments[index+1].startAngle = segment.endAngle; 3124 | } 3125 | segment.draw(); 3126 | }, this); 3127 | this.scale.draw(); 3128 | } 3129 | }); 3130 | 3131 | }).call(this); 3132 | (function(){ 3133 | "use strict"; 3134 | 3135 | var root = this, 3136 | Chart = root.Chart, 3137 | helpers = Chart.helpers; 3138 | 3139 | 3140 | 3141 | Chart.Type.extend({ 3142 | name: "Radar", 3143 | defaults:{ 3144 | //Boolean - Whether to show lines for each scale point 3145 | scaleShowLine : true, 3146 | 3147 | //Boolean - Whether we show the angle lines out of the radar 3148 | angleShowLineOut : true, 3149 | 3150 | //Boolean - Whether to show labels on the scale 3151 | scaleShowLabels : false, 3152 | 3153 | // Boolean - Whether the scale should begin at zero 3154 | scaleBeginAtZero : true, 3155 | 3156 | //String - Colour of the angle line 3157 | angleLineColor : "rgba(0,0,0,.1)", 3158 | 3159 | //Number - Pixel width of the angle line 3160 | angleLineWidth : 1, 3161 | 3162 | //String - Point label font declaration 3163 | pointLabelFontFamily : "'Arial'", 3164 | 3165 | //String - Point label font weight 3166 | pointLabelFontStyle : "normal", 3167 | 3168 | //Number - Point label font size in pixels 3169 | pointLabelFontSize : 10, 3170 | 3171 | //String - Point label font colour 3172 | pointLabelFontColor : "#666", 3173 | 3174 | //Boolean - Whether to show a dot for each point 3175 | pointDot : true, 3176 | 3177 | //Number - Radius of each point dot in pixels 3178 | pointDotRadius : 3, 3179 | 3180 | //Number - Pixel width of point dot stroke 3181 | pointDotStrokeWidth : 1, 3182 | 3183 | //Number - amount extra to add to the radius to cater for hit detection outside the drawn point 3184 | pointHitDetectionRadius : 20, 3185 | 3186 | //Boolean - Whether to show a stroke for datasets 3187 | datasetStroke : true, 3188 | 3189 | //Number - Pixel width of dataset stroke 3190 | datasetStrokeWidth : 2, 3191 | 3192 | //Boolean - Whether to fill the dataset with a colour 3193 | datasetFill : true, 3194 | 3195 | //String - A legend template 3196 | legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" 3197 | 3198 | }, 3199 | 3200 | initialize: function(data){ 3201 | this.PointClass = Chart.Point.extend({ 3202 | strokeWidth : this.options.pointDotStrokeWidth, 3203 | radius : this.options.pointDotRadius, 3204 | display: this.options.pointDot, 3205 | hitDetectionRadius : this.options.pointHitDetectionRadius, 3206 | ctx : this.chart.ctx 3207 | }); 3208 | 3209 | this.datasets = []; 3210 | 3211 | this.buildScale(data); 3212 | 3213 | //Set up tooltip events on the chart 3214 | if (this.options.showTooltips){ 3215 | helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ 3216 | var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; 3217 | 3218 | this.eachPoints(function(point){ 3219 | point.restore(['fillColor', 'strokeColor']); 3220 | }); 3221 | helpers.each(activePointsCollection, function(activePoint){ 3222 | activePoint.fillColor = activePoint.highlightFill; 3223 | activePoint.strokeColor = activePoint.highlightStroke; 3224 | }); 3225 | 3226 | this.showTooltip(activePointsCollection); 3227 | }); 3228 | } 3229 | 3230 | //Iterate through each of the datasets, and build this into a property of the chart 3231 | helpers.each(data.datasets,function(dataset){ 3232 | 3233 | var datasetObject = { 3234 | label: dataset.label || null, 3235 | fillColor : dataset.fillColor, 3236 | strokeColor : dataset.strokeColor, 3237 | pointColor : dataset.pointColor, 3238 | pointStrokeColor : dataset.pointStrokeColor, 3239 | points : [] 3240 | }; 3241 | 3242 | this.datasets.push(datasetObject); 3243 | 3244 | helpers.each(dataset.data,function(dataPoint,index){ 3245 | //Add a new point for each piece of data, passing any required data to draw. 3246 | var pointPosition; 3247 | if (!this.scale.animation){ 3248 | pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); 3249 | } 3250 | datasetObject.points.push(new this.PointClass({ 3251 | value : dataPoint, 3252 | label : data.labels[index], 3253 | datasetLabel: dataset.label, 3254 | x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, 3255 | y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, 3256 | strokeColor : dataset.pointStrokeColor, 3257 | fillColor : dataset.pointColor, 3258 | highlightFill : dataset.pointHighlightFill || dataset.pointColor, 3259 | highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor 3260 | })); 3261 | },this); 3262 | 3263 | },this); 3264 | 3265 | this.render(); 3266 | }, 3267 | eachPoints : function(callback){ 3268 | helpers.each(this.datasets,function(dataset){ 3269 | helpers.each(dataset.points,callback,this); 3270 | },this); 3271 | }, 3272 | 3273 | getPointsAtEvent : function(evt){ 3274 | var mousePosition = helpers.getRelativePosition(evt), 3275 | fromCenter = helpers.getAngleFromPoint({ 3276 | x: this.scale.xCenter, 3277 | y: this.scale.yCenter 3278 | }, mousePosition); 3279 | 3280 | var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, 3281 | pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), 3282 | activePointsCollection = []; 3283 | 3284 | // If we're at the top, make the pointIndex 0 to get the first of the array. 3285 | if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ 3286 | pointIndex = 0; 3287 | } 3288 | 3289 | if (fromCenter.distance <= this.scale.drawingArea){ 3290 | helpers.each(this.datasets, function(dataset){ 3291 | activePointsCollection.push(dataset.points[pointIndex]); 3292 | }); 3293 | } 3294 | 3295 | return activePointsCollection; 3296 | }, 3297 | 3298 | buildScale : function(data){ 3299 | this.scale = new Chart.RadialScale({ 3300 | display: this.options.showScale, 3301 | fontStyle: this.options.scaleFontStyle, 3302 | fontSize: this.options.scaleFontSize, 3303 | fontFamily: this.options.scaleFontFamily, 3304 | fontColor: this.options.scaleFontColor, 3305 | showLabels: this.options.scaleShowLabels, 3306 | showLabelBackdrop: this.options.scaleShowLabelBackdrop, 3307 | backdropColor: this.options.scaleBackdropColor, 3308 | backdropPaddingY : this.options.scaleBackdropPaddingY, 3309 | backdropPaddingX: this.options.scaleBackdropPaddingX, 3310 | lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, 3311 | lineColor: this.options.scaleLineColor, 3312 | angleLineColor : this.options.angleLineColor, 3313 | angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, 3314 | // Point labels at the edge of each line 3315 | pointLabelFontColor : this.options.pointLabelFontColor, 3316 | pointLabelFontSize : this.options.pointLabelFontSize, 3317 | pointLabelFontFamily : this.options.pointLabelFontFamily, 3318 | pointLabelFontStyle : this.options.pointLabelFontStyle, 3319 | height : this.chart.height, 3320 | width: this.chart.width, 3321 | xCenter: this.chart.width/2, 3322 | yCenter: this.chart.height/2, 3323 | ctx : this.chart.ctx, 3324 | templateString: this.options.scaleLabel, 3325 | labels: data.labels, 3326 | valuesCount: data.datasets[0].data.length 3327 | }); 3328 | 3329 | this.scale.setScaleSize(); 3330 | this.updateScaleRange(data.datasets); 3331 | this.scale.buildYLabels(); 3332 | }, 3333 | updateScaleRange: function(datasets){ 3334 | var valuesArray = (function(){ 3335 | var totalDataArray = []; 3336 | helpers.each(datasets,function(dataset){ 3337 | if (dataset.data){ 3338 | totalDataArray = totalDataArray.concat(dataset.data); 3339 | } 3340 | else { 3341 | helpers.each(dataset.points, function(point){ 3342 | totalDataArray.push(point.value); 3343 | }); 3344 | } 3345 | }); 3346 | return totalDataArray; 3347 | })(); 3348 | 3349 | 3350 | var scaleSizes = (this.options.scaleOverride) ? 3351 | { 3352 | steps: this.options.scaleSteps, 3353 | stepValue: this.options.scaleStepWidth, 3354 | min: this.options.scaleStartValue, 3355 | max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) 3356 | } : 3357 | helpers.calculateScaleRange( 3358 | valuesArray, 3359 | helpers.min([this.chart.width, this.chart.height])/2, 3360 | this.options.scaleFontSize, 3361 | this.options.scaleBeginAtZero, 3362 | this.options.scaleIntegersOnly 3363 | ); 3364 | 3365 | helpers.extend( 3366 | this.scale, 3367 | scaleSizes 3368 | ); 3369 | 3370 | }, 3371 | addData : function(valuesArray,label){ 3372 | //Map the values array for each of the datasets 3373 | this.scale.valuesCount++; 3374 | helpers.each(valuesArray,function(value,datasetIndex){ 3375 | var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); 3376 | this.datasets[datasetIndex].points.push(new this.PointClass({ 3377 | value : value, 3378 | label : label, 3379 | x: pointPosition.x, 3380 | y: pointPosition.y, 3381 | strokeColor : this.datasets[datasetIndex].pointStrokeColor, 3382 | fillColor : this.datasets[datasetIndex].pointColor 3383 | })); 3384 | },this); 3385 | 3386 | this.scale.labels.push(label); 3387 | 3388 | this.reflow(); 3389 | 3390 | this.update(); 3391 | }, 3392 | removeData : function(){ 3393 | this.scale.valuesCount--; 3394 | this.scale.labels.shift(); 3395 | helpers.each(this.datasets,function(dataset){ 3396 | dataset.points.shift(); 3397 | },this); 3398 | this.reflow(); 3399 | this.update(); 3400 | }, 3401 | update : function(){ 3402 | this.eachPoints(function(point){ 3403 | point.save(); 3404 | }); 3405 | this.reflow(); 3406 | this.render(); 3407 | }, 3408 | reflow: function(){ 3409 | helpers.extend(this.scale, { 3410 | width : this.chart.width, 3411 | height: this.chart.height, 3412 | size : helpers.min([this.chart.width, this.chart.height]), 3413 | xCenter: this.chart.width/2, 3414 | yCenter: this.chart.height/2 3415 | }); 3416 | this.updateScaleRange(this.datasets); 3417 | this.scale.setScaleSize(); 3418 | this.scale.buildYLabels(); 3419 | }, 3420 | draw : function(ease){ 3421 | var easeDecimal = ease || 1, 3422 | ctx = this.chart.ctx; 3423 | this.clear(); 3424 | this.scale.draw(); 3425 | 3426 | helpers.each(this.datasets,function(dataset){ 3427 | 3428 | //Transition each point first so that the line and point drawing isn't out of sync 3429 | helpers.each(dataset.points,function(point,index){ 3430 | if (point.hasValue()){ 3431 | point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); 3432 | } 3433 | },this); 3434 | 3435 | 3436 | 3437 | //Draw the line between all the points 3438 | ctx.lineWidth = this.options.datasetStrokeWidth; 3439 | ctx.strokeStyle = dataset.strokeColor; 3440 | ctx.beginPath(); 3441 | helpers.each(dataset.points,function(point,index){ 3442 | if (index === 0){ 3443 | ctx.moveTo(point.x,point.y); 3444 | } 3445 | else{ 3446 | ctx.lineTo(point.x,point.y); 3447 | } 3448 | },this); 3449 | ctx.closePath(); 3450 | ctx.stroke(); 3451 | 3452 | ctx.fillStyle = dataset.fillColor; 3453 | ctx.fill(); 3454 | 3455 | //Now draw the points over the line 3456 | //A little inefficient double looping, but better than the line 3457 | //lagging behind the point positions 3458 | helpers.each(dataset.points,function(point){ 3459 | if (point.hasValue()){ 3460 | point.draw(); 3461 | } 3462 | }); 3463 | 3464 | },this); 3465 | 3466 | } 3467 | 3468 | }); 3469 | 3470 | 3471 | 3472 | 3473 | 3474 | }).call(this); --------------------------------------------------------------------------------