File manager - Edit - /home/newsbmcs.com/public_html/static/img/logo/charts.tar
Back
morris/morris.js 0000644 00000231545 15030376016 0007743 0 ustar 00 /* @license morris.js v0.5.0 Copyright 2014 Olly Smith All rights reserved. Licensed under the BSD-2-Clause License. */ (function() { var $, Morris, minutesSpecHelper, secondsSpecHelper, __slice = [].slice, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; Morris = window.Morris = {}; $ = jQuery; Morris.EventEmitter = (function() { function EventEmitter() {} EventEmitter.prototype.on = function(name, handler) { if (this.handlers == null) { this.handlers = {}; } if (this.handlers[name] == null) { this.handlers[name] = []; } this.handlers[name].push(handler); return this; }; EventEmitter.prototype.fire = function() { var args, handler, name, _i, _len, _ref, _results; name = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; if ((this.handlers != null) && (this.handlers[name] != null)) { _ref = this.handlers[name]; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { handler = _ref[_i]; _results.push(handler.apply(null, args)); } return _results; } }; return EventEmitter; })(); Morris.commas = function(num) { var absnum, intnum, ret, strabsnum; if (num != null) { ret = num < 0 ? "-" : ""; absnum = Math.abs(num); intnum = Math.floor(absnum).toFixed(0); ret += intnum.replace(/(?=(?:\d{3})+$)(?!^)/g, ','); strabsnum = absnum.toString(); if (strabsnum.length > intnum.length) { ret += strabsnum.slice(intnum.length); } return ret; } else { return '-'; } }; Morris.pad2 = function(number) { return (number < 10 ? '0' : '') + number; }; Morris.Grid = (function(_super) { __extends(Grid, _super); function Grid(options) { this.resizeHandler = __bind(this.resizeHandler, this); var _this = this; if (typeof options.element === 'string') { this.el = $(document.getElementById(options.element)); } else { this.el = $(options.element); } if ((this.el == null) || this.el.length === 0) { throw new Error("Graph container element not found"); } if (this.el.css('position') === 'static') { this.el.css('position', 'relative'); } this.options = $.extend({}, this.gridDefaults, this.defaults || {}, options); if (typeof this.options.units === 'string') { this.options.postUnits = options.units; } this.raphael = new Raphael(this.el[0]); this.elementWidth = null; this.elementHeight = null; this.dirty = false; this.selectFrom = null; if (this.init) { this.init(); } this.setData(this.options.data); this.el.bind('mousemove', function(evt) { var left, offset, right, width, x; offset = _this.el.offset(); x = evt.pageX - offset.left; if (_this.selectFrom) { left = _this.data[_this.hitTest(Math.min(x, _this.selectFrom))]._x; right = _this.data[_this.hitTest(Math.max(x, _this.selectFrom))]._x; width = right - left; return _this.selectionRect.attr({ x: left, width: width }); } else { return _this.fire('hovermove', x, evt.pageY - offset.top); } }); this.el.bind('mouseleave', function(evt) { if (_this.selectFrom) { _this.selectionRect.hide(); _this.selectFrom = null; } return _this.fire('hoverout'); }); this.el.bind('touchstart touchmove touchend', function(evt) { var offset, touch; touch = evt.originalEvent.touches[0] || evt.originalEvent.changedTouches[0]; offset = _this.el.offset(); return _this.fire('hovermove', touch.pageX - offset.left, touch.pageY - offset.top); }); this.el.bind('click', function(evt) { var offset; offset = _this.el.offset(); return _this.fire('gridclick', evt.pageX - offset.left, evt.pageY - offset.top); }); if (this.options.rangeSelect) { this.selectionRect = this.raphael.rect(0, 0, 0, this.el.innerHeight()).attr({ fill: this.options.rangeSelectColor, stroke: false }).toBack().hide(); this.el.bind('mousedown', function(evt) { var offset; offset = _this.el.offset(); return _this.startRange(evt.pageX - offset.left); }); this.el.bind('mouseup', function(evt) { var offset; offset = _this.el.offset(); _this.endRange(evt.pageX - offset.left); return _this.fire('hovermove', evt.pageX - offset.left, evt.pageY - offset.top); }); } if (this.options.resize) { $(window).bind('resize', function(evt) { if (_this.timeoutId != null) { window.clearTimeout(_this.timeoutId); } return _this.timeoutId = window.setTimeout(_this.resizeHandler, 100); }); } this.el.css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)'); if (this.postInit) { this.postInit(); } } Grid.prototype.gridDefaults = { dateFormat: null, axes: true, grid: true, gridLineColor: '#aaa', gridStrokeWidth: 0.5, gridTextColor: '#888', gridTextSize: 12, gridTextFamily: 'sans-serif', gridTextWeight: 'normal', hideHover: false, yLabelFormat: null, xLabelAngle: 0, numLines: 5, padding: 25, parseTime: true, postUnits: '', preUnits: '', ymax: 'auto', ymin: 'auto 0', goals: [], goalStrokeWidth: 1.0, goalLineColors: ['#666633', '#999966', '#cc6666', '#663333'], events: [], eventStrokeWidth: 1.0, eventLineColors: ['#005a04', '#ccffbb', '#3a5f0b', '#005502'], rangeSelect: null, rangeSelectColor: '#eef', resize: false }; Grid.prototype.setData = function(data, redraw) { var e, idx, index, maxGoal, minGoal, ret, row, step, total, y, ykey, ymax, ymin, yval, _ref; if (redraw == null) { redraw = true; } this.options.data = data; if ((data == null) || data.length === 0) { this.data = []; this.raphael.clear(); if (this.hover != null) { this.hover.hide(); } return; } ymax = this.cumulative ? 0 : null; ymin = this.cumulative ? 0 : null; if (this.options.goals.length > 0) { minGoal = Math.min.apply(Math, this.options.goals); maxGoal = Math.max.apply(Math, this.options.goals); ymin = ymin != null ? Math.min(ymin, minGoal) : minGoal; ymax = ymax != null ? Math.max(ymax, maxGoal) : maxGoal; } this.data = (function() { var _i, _len, _results; _results = []; for (index = _i = 0, _len = data.length; _i < _len; index = ++_i) { row = data[index]; ret = { src: row }; ret.label = row[this.options.xkey]; if (this.options.parseTime) { ret.x = Morris.parseDate(ret.label); if (this.options.dateFormat) { ret.label = this.options.dateFormat(ret.x); } else if (typeof ret.label === 'number') { ret.label = new Date(ret.label).toString(); } } else { ret.x = index; if (this.options.xLabelFormat) { ret.label = this.options.xLabelFormat(ret); } } total = 0; ret.y = (function() { var _j, _len1, _ref, _results1; _ref = this.options.ykeys; _results1 = []; for (idx = _j = 0, _len1 = _ref.length; _j < _len1; idx = ++_j) { ykey = _ref[idx]; yval = row[ykey]; if (typeof yval === 'string') { yval = parseFloat(yval); } if ((yval != null) && typeof yval !== 'number') { yval = null; } if (yval != null) { if (this.cumulative) { total += yval; } else { if (ymax != null) { ymax = Math.max(yval, ymax); ymin = Math.min(yval, ymin); } else { ymax = ymin = yval; } } } if (this.cumulative && (total != null)) { ymax = Math.max(total, ymax); ymin = Math.min(total, ymin); } _results1.push(yval); } return _results1; }).call(this); _results.push(ret); } return _results; }).call(this); if (this.options.parseTime) { this.data = this.data.sort(function(a, b) { return (a.x > b.x) - (b.x > a.x); }); } this.xmin = this.data[0].x; this.xmax = this.data[this.data.length - 1].x; this.events = []; if (this.options.events.length > 0) { if (this.options.parseTime) { this.events = (function() { var _i, _len, _ref, _results; _ref = this.options.events; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { e = _ref[_i]; _results.push(Morris.parseDate(e)); } return _results; }).call(this); } else { this.events = this.options.events; } this.xmax = Math.max(this.xmax, Math.max.apply(Math, this.events)); this.xmin = Math.min(this.xmin, Math.min.apply(Math, this.events)); } if (this.xmin === this.xmax) { this.xmin -= 1; this.xmax += 1; } this.ymin = this.yboundary('min', ymin); this.ymax = this.yboundary('max', ymax); if (this.ymin === this.ymax) { if (ymin) { this.ymin -= 1; } this.ymax += 1; } if (((_ref = this.options.axes) === true || _ref === 'both' || _ref === 'y') || this.options.grid === true) { if (this.options.ymax === this.gridDefaults.ymax && this.options.ymin === this.gridDefaults.ymin) { this.grid = this.autoGridLines(this.ymin, this.ymax, this.options.numLines); this.ymin = Math.min(this.ymin, this.grid[0]); this.ymax = Math.max(this.ymax, this.grid[this.grid.length - 1]); } else { step = (this.ymax - this.ymin) / (this.options.numLines - 1); this.grid = (function() { var _i, _ref1, _ref2, _results; _results = []; for (y = _i = _ref1 = this.ymin, _ref2 = this.ymax; step > 0 ? _i <= _ref2 : _i >= _ref2; y = _i += step) { _results.push(y); } return _results; }).call(this); } } this.dirty = true; if (redraw) { return this.redraw(); } }; Grid.prototype.yboundary = function(boundaryType, currentValue) { var boundaryOption, suggestedValue; boundaryOption = this.options["y" + boundaryType]; if (typeof boundaryOption === 'string') { if (boundaryOption.slice(0, 4) === 'auto') { if (boundaryOption.length > 5) { suggestedValue = parseInt(boundaryOption.slice(5), 10); if (currentValue == null) { return suggestedValue; } return Math[boundaryType](currentValue, suggestedValue); } else { if (currentValue != null) { return currentValue; } else { return 0; } } } else { return parseInt(boundaryOption, 10); } } else { return boundaryOption; } }; Grid.prototype.autoGridLines = function(ymin, ymax, nlines) { var gmax, gmin, grid, smag, span, step, unit, y, ymag; span = ymax - ymin; ymag = Math.floor(Math.log(span) / Math.log(10)); unit = Math.pow(10, ymag); gmin = Math.floor(ymin / unit) * unit; gmax = Math.ceil(ymax / unit) * unit; step = (gmax - gmin) / (nlines - 1); if (unit === 1 && step > 1 && Math.ceil(step) !== step) { step = Math.ceil(step); gmax = gmin + step * (nlines - 1); } if (gmin < 0 && gmax > 0) { gmin = Math.floor(ymin / step) * step; gmax = Math.ceil(ymax / step) * step; } if (step < 1) { smag = Math.floor(Math.log(step) / Math.log(10)); grid = (function() { var _i, _results; _results = []; for (y = _i = gmin; step > 0 ? _i <= gmax : _i >= gmax; y = _i += step) { _results.push(parseFloat(y.toFixed(1 - smag))); } return _results; })(); } else { grid = (function() { var _i, _results; _results = []; for (y = _i = gmin; step > 0 ? _i <= gmax : _i >= gmax; y = _i += step) { _results.push(y); } return _results; })(); } return grid; }; Grid.prototype._calc = function() { var bottomOffsets, gridLine, h, i, w, yLabelWidths, _ref, _ref1; w = this.el.width(); h = this.el.height(); if (this.elementWidth !== w || this.elementHeight !== h || this.dirty) { this.elementWidth = w; this.elementHeight = h; this.dirty = false; this.left = this.options.padding; this.right = this.elementWidth - this.options.padding; this.top = this.options.padding; this.bottom = this.elementHeight - this.options.padding; if ((_ref = this.options.axes) === true || _ref === 'both' || _ref === 'y') { yLabelWidths = (function() { var _i, _len, _ref1, _results; _ref1 = this.grid; _results = []; for (_i = 0, _len = _ref1.length; _i < _len; _i++) { gridLine = _ref1[_i]; _results.push(this.measureText(this.yAxisFormat(gridLine)).width); } return _results; }).call(this); this.left += Math.max.apply(Math, yLabelWidths); } if ((_ref1 = this.options.axes) === true || _ref1 === 'both' || _ref1 === 'x') { bottomOffsets = (function() { var _i, _ref2, _results; _results = []; for (i = _i = 0, _ref2 = this.data.length; 0 <= _ref2 ? _i < _ref2 : _i > _ref2; i = 0 <= _ref2 ? ++_i : --_i) { _results.push(this.measureText(this.data[i].text, -this.options.xLabelAngle).height); } return _results; }).call(this); this.bottom -= Math.max.apply(Math, bottomOffsets); } this.width = Math.max(1, this.right - this.left); this.height = Math.max(1, this.bottom - this.top); this.dx = this.width / (this.xmax - this.xmin); this.dy = this.height / (this.ymax - this.ymin); if (this.calc) { return this.calc(); } } }; Grid.prototype.transY = function(y) { return this.bottom - (y - this.ymin) * this.dy; }; Grid.prototype.transX = function(x) { if (this.data.length === 1) { return (this.left + this.right) / 2; } else { return this.left + (x - this.xmin) * this.dx; } }; Grid.prototype.redraw = function() { this.raphael.clear(); this._calc(); this.drawGrid(); this.drawGoals(); this.drawEvents(); if (this.draw) { return this.draw(); } }; Grid.prototype.measureText = function(text, angle) { var ret, tt; if (angle == null) { angle = 0; } tt = this.raphael.text(100, 100, text).attr('font-size', this.options.gridTextSize).attr('font-family', this.options.gridTextFamily).attr('font-weight', this.options.gridTextWeight).rotate(angle); ret = tt.getBBox(); tt.remove(); return ret; }; Grid.prototype.yAxisFormat = function(label) { return this.yLabelFormat(label); }; Grid.prototype.yLabelFormat = function(label) { if (typeof this.options.yLabelFormat === 'function') { return this.options.yLabelFormat(label); } else { return "" + this.options.preUnits + (Morris.commas(label)) + this.options.postUnits; } }; Grid.prototype.drawGrid = function() { var lineY, y, _i, _len, _ref, _ref1, _ref2, _results; if (this.options.grid === false && ((_ref = this.options.axes) !== true && _ref !== 'both' && _ref !== 'y')) { return; } _ref1 = this.grid; _results = []; for (_i = 0, _len = _ref1.length; _i < _len; _i++) { lineY = _ref1[_i]; y = this.transY(lineY); if ((_ref2 = this.options.axes) === true || _ref2 === 'both' || _ref2 === 'y') { this.drawYAxisLabel(this.left - this.options.padding / 2, y, this.yAxisFormat(lineY)); } if (this.options.grid) { _results.push(this.drawGridLine("M" + this.left + "," + y + "H" + (this.left + this.width))); } else { _results.push(void 0); } } return _results; }; Grid.prototype.drawGoals = function() { var color, goal, i, _i, _len, _ref, _results; _ref = this.options.goals; _results = []; for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { goal = _ref[i]; color = this.options.goalLineColors[i % this.options.goalLineColors.length]; _results.push(this.drawGoal(goal, color)); } return _results; }; Grid.prototype.drawEvents = function() { var color, event, i, _i, _len, _ref, _results; _ref = this.events; _results = []; for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { event = _ref[i]; color = this.options.eventLineColors[i % this.options.eventLineColors.length]; _results.push(this.drawEvent(event, color)); } return _results; }; Grid.prototype.drawGoal = function(goal, color) { return this.raphael.path("M" + this.left + "," + (this.transY(goal)) + "H" + this.right).attr('stroke', color).attr('stroke-width', this.options.goalStrokeWidth); }; Grid.prototype.drawEvent = function(event, color) { return this.raphael.path("M" + (this.transX(event)) + "," + this.bottom + "V" + this.top).attr('stroke', color).attr('stroke-width', this.options.eventStrokeWidth); }; Grid.prototype.drawYAxisLabel = function(xPos, yPos, text) { return this.raphael.text(xPos, yPos, text).attr('font-size', this.options.gridTextSize).attr('font-family', this.options.gridTextFamily).attr('font-weight', this.options.gridTextWeight).attr('fill', this.options.gridTextColor).attr('text-anchor', 'end'); }; Grid.prototype.drawGridLine = function(path) { return this.raphael.path(path).attr('stroke', this.options.gridLineColor).attr('stroke-width', this.options.gridStrokeWidth); }; Grid.prototype.startRange = function(x) { this.hover.hide(); this.selectFrom = x; return this.selectionRect.attr({ x: x, width: 0 }).show(); }; Grid.prototype.endRange = function(x) { var end, start; if (this.selectFrom) { start = Math.min(this.selectFrom, x); end = Math.max(this.selectFrom, x); this.options.rangeSelect.call(this.el, { start: this.data[this.hitTest(start)].x, end: this.data[this.hitTest(end)].x }); return this.selectFrom = null; } }; Grid.prototype.resizeHandler = function() { this.timeoutId = null; this.raphael.setSize(this.el.width(), this.el.height()); return this.redraw(); }; return Grid; })(Morris.EventEmitter); Morris.parseDate = function(date) { var isecs, m, msecs, n, o, offsetmins, p, q, r, ret, secs; if (typeof date === 'number') { return date; } m = date.match(/^(\d+) Q(\d)$/); n = date.match(/^(\d+)-(\d+)$/); o = date.match(/^(\d+)-(\d+)-(\d+)$/); p = date.match(/^(\d+) W(\d+)$/); q = date.match(/^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+)(Z|([+-])(\d\d):?(\d\d))?$/); r = date.match(/^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+):(\d+(\.\d+)?)(Z|([+-])(\d\d):?(\d\d))?$/); if (m) { return new Date(parseInt(m[1], 10), parseInt(m[2], 10) * 3 - 1, 1).getTime(); } else if (n) { return new Date(parseInt(n[1], 10), parseInt(n[2], 10) - 1, 1).getTime(); } else if (o) { return new Date(parseInt(o[1], 10), parseInt(o[2], 10) - 1, parseInt(o[3], 10)).getTime(); } else if (p) { ret = new Date(parseInt(p[1], 10), 0, 1); if (ret.getDay() !== 4) { ret.setMonth(0, 1 + ((4 - ret.getDay()) + 7) % 7); } return ret.getTime() + parseInt(p[2], 10) * 604800000; } else if (q) { if (!q[6]) { return new Date(parseInt(q[1], 10), parseInt(q[2], 10) - 1, parseInt(q[3], 10), parseInt(q[4], 10), parseInt(q[5], 10)).getTime(); } else { offsetmins = 0; if (q[6] !== 'Z') { offsetmins = parseInt(q[8], 10) * 60 + parseInt(q[9], 10); if (q[7] === '+') { offsetmins = 0 - offsetmins; } } return Date.UTC(parseInt(q[1], 10), parseInt(q[2], 10) - 1, parseInt(q[3], 10), parseInt(q[4], 10), parseInt(q[5], 10) + offsetmins); } } else if (r) { secs = parseFloat(r[6]); isecs = Math.floor(secs); msecs = Math.round((secs - isecs) * 1000); if (!r[8]) { return new Date(parseInt(r[1], 10), parseInt(r[2], 10) - 1, parseInt(r[3], 10), parseInt(r[4], 10), parseInt(r[5], 10), isecs, msecs).getTime(); } else { offsetmins = 0; if (r[8] !== 'Z') { offsetmins = parseInt(r[10], 10) * 60 + parseInt(r[11], 10); if (r[9] === '+') { offsetmins = 0 - offsetmins; } } return Date.UTC(parseInt(r[1], 10), parseInt(r[2], 10) - 1, parseInt(r[3], 10), parseInt(r[4], 10), parseInt(r[5], 10) + offsetmins, isecs, msecs); } } else { return new Date(parseInt(date, 10), 0, 1).getTime(); } }; Morris.Hover = (function() { Hover.defaults = { "class": 'morris-hover morris-default-style' }; function Hover(options) { if (options == null) { options = {}; } this.options = $.extend({}, Morris.Hover.defaults, options); this.el = $("<div class='" + this.options["class"] + "'></div>"); this.el.hide(); this.options.parent.append(this.el); } Hover.prototype.update = function(html, x, y) { if (!html) { return this.hide(); } else { this.html(html); this.show(); return this.moveTo(x, y); } }; Hover.prototype.html = function(content) { return this.el.html(content); }; Hover.prototype.moveTo = function(x, y) { var hoverHeight, hoverWidth, left, parentHeight, parentWidth, top; parentWidth = this.options.parent.innerWidth(); parentHeight = this.options.parent.innerHeight(); hoverWidth = this.el.outerWidth(); hoverHeight = this.el.outerHeight(); left = Math.min(Math.max(0, x - hoverWidth / 2), parentWidth - hoverWidth); if (y != null) { top = y - hoverHeight - 10; if (top < 0) { top = y + 10; if (top + hoverHeight > parentHeight) { top = parentHeight / 2 - hoverHeight / 2; } } } else { top = parentHeight / 2 - hoverHeight / 2; } return this.el.css({ left: left + "px", top: parseInt(top) + "px" }); }; Hover.prototype.show = function() { return this.el.show(); }; Hover.prototype.hide = function() { return this.el.hide(); }; return Hover; })(); Morris.Line = (function(_super) { __extends(Line, _super); function Line(options) { this.hilight = __bind(this.hilight, this); this.onHoverOut = __bind(this.onHoverOut, this); this.onHoverMove = __bind(this.onHoverMove, this); this.onGridClick = __bind(this.onGridClick, this); if (!(this instanceof Morris.Line)) { return new Morris.Line(options); } Line.__super__.constructor.call(this, options); } Line.prototype.init = function() { if (this.options.hideHover !== 'always') { this.hover = new Morris.Hover({ parent: this.el }); this.on('hovermove', this.onHoverMove); this.on('hoverout', this.onHoverOut); return this.on('gridclick', this.onGridClick); } }; Line.prototype.defaults = { lineWidth: 3, pointSize: 4, lineColors: ['#0b62a4', '#7A92A3', '#4da74d', '#afd8f8', '#edc240', '#cb4b4b', '#9440ed'], pointStrokeWidths: [1], pointStrokeColors: ['#ffffff'], pointFillColors: [], smooth: true, xLabels: 'auto', xLabelFormat: null, xLabelMargin: 24, hideHover: false }; Line.prototype.calc = function() { this.calcPoints(); return this.generatePaths(); }; Line.prototype.calcPoints = function() { var row, y, _i, _len, _ref, _results; _ref = this.data; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { row = _ref[_i]; row._x = this.transX(row.x); row._y = (function() { var _j, _len1, _ref1, _results1; _ref1 = row.y; _results1 = []; for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { y = _ref1[_j]; if (y != null) { _results1.push(this.transY(y)); } else { _results1.push(y); } } return _results1; }).call(this); _results.push(row._ymax = Math.min.apply(Math, [this.bottom].concat((function() { var _j, _len1, _ref1, _results1; _ref1 = row._y; _results1 = []; for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { y = _ref1[_j]; if (y != null) { _results1.push(y); } } return _results1; })()))); } return _results; }; Line.prototype.hitTest = function(x) { var index, r, _i, _len, _ref; if (this.data.length === 0) { return null; } _ref = this.data.slice(1); for (index = _i = 0, _len = _ref.length; _i < _len; index = ++_i) { r = _ref[index]; if (x < (r._x + this.data[index]._x) / 2) { break; } } return index; }; Line.prototype.onGridClick = function(x, y) { var index; index = this.hitTest(x); return this.fire('click', index, this.data[index].src, x, y); }; Line.prototype.onHoverMove = function(x, y) { var index; index = this.hitTest(x); return this.displayHoverForRow(index); }; Line.prototype.onHoverOut = function() { if (this.options.hideHover !== false) { return this.displayHoverForRow(null); } }; Line.prototype.displayHoverForRow = function(index) { var _ref; if (index != null) { (_ref = this.hover).update.apply(_ref, this.hoverContentForRow(index)); return this.hilight(index); } else { this.hover.hide(); return this.hilight(); } }; Line.prototype.hoverContentForRow = function(index) { var content, j, row, y, _i, _len, _ref; row = this.data[index]; content = "<div class='morris-hover-row-label'>" + row.label + "</div>"; _ref = row.y; for (j = _i = 0, _len = _ref.length; _i < _len; j = ++_i) { y = _ref[j]; content += "<div class='morris-hover-point' style='color: " + (this.colorFor(row, j, 'label')) + "'>\n " + this.options.labels[j] + ":\n " + (this.yLabelFormat(y)) + "\n</div>"; } if (typeof this.options.hoverCallback === 'function') { content = this.options.hoverCallback(index, this.options, content, row.src); } return [content, row._x, row._ymax]; }; Line.prototype.generatePaths = function() { var coords, i, r, smooth; return this.paths = (function() { var _i, _ref, _ref1, _results; _results = []; for (i = _i = 0, _ref = this.options.ykeys.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { smooth = typeof this.options.smooth === "boolean" ? this.options.smooth : (_ref1 = this.options.ykeys[i], __indexOf.call(this.options.smooth, _ref1) >= 0); coords = (function() { var _j, _len, _ref2, _results1; _ref2 = this.data; _results1 = []; for (_j = 0, _len = _ref2.length; _j < _len; _j++) { r = _ref2[_j]; if (r._y[i] !== void 0) { _results1.push({ x: r._x, y: r._y[i] }); } } return _results1; }).call(this); if (coords.length > 1) { _results.push(Morris.Line.createPath(coords, smooth, this.bottom)); } else { _results.push(null); } } return _results; }).call(this); }; Line.prototype.draw = function() { var _ref; if ((_ref = this.options.axes) === true || _ref === 'both' || _ref === 'x') { this.drawXAxis(); } this.drawSeries(); if (this.options.hideHover === false) { return this.displayHoverForRow(this.data.length - 1); } }; Line.prototype.drawXAxis = function() { var drawLabel, l, labels, prevAngleMargin, prevLabelMargin, row, ypos, _i, _len, _results, _this = this; ypos = this.bottom + this.options.padding / 2; prevLabelMargin = null; prevAngleMargin = null; drawLabel = function(labelText, xpos) { var label, labelBox, margin, offset, textBox; label = _this.drawXAxisLabel(_this.transX(xpos), ypos, labelText); textBox = label.getBBox(); label.transform("r" + (-_this.options.xLabelAngle)); labelBox = label.getBBox(); label.transform("t0," + (labelBox.height / 2) + "..."); if (_this.options.xLabelAngle !== 0) { offset = -0.5 * textBox.width * Math.cos(_this.options.xLabelAngle * Math.PI / 180.0); label.transform("t" + offset + ",0..."); } labelBox = label.getBBox(); if (((prevLabelMargin == null) || prevLabelMargin >= labelBox.x + labelBox.width || (prevAngleMargin != null) && prevAngleMargin >= labelBox.x) && labelBox.x >= 0 && (labelBox.x + labelBox.width) < _this.el.width()) { if (_this.options.xLabelAngle !== 0) { margin = 1.25 * _this.options.gridTextSize / Math.sin(_this.options.xLabelAngle * Math.PI / 180.0); prevAngleMargin = labelBox.x - margin; } return prevLabelMargin = labelBox.x - _this.options.xLabelMargin; } else { return label.remove(); } }; if (this.options.parseTime) { if (this.data.length === 1 && this.options.xLabels === 'auto') { labels = [[this.data[0].label, this.data[0].x]]; } else { labels = Morris.labelSeries(this.xmin, this.xmax, this.width, this.options.xLabels, this.options.xLabelFormat); } } else { labels = (function() { var _i, _len, _ref, _results; _ref = this.data; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { row = _ref[_i]; _results.push([row.label, row.x]); } return _results; }).call(this); } labels.reverse(); _results = []; for (_i = 0, _len = labels.length; _i < _len; _i++) { l = labels[_i]; _results.push(drawLabel(l[0], l[1])); } return _results; }; Line.prototype.drawSeries = function() { var i, _i, _j, _ref, _ref1, _results; this.seriesPoints = []; for (i = _i = _ref = this.options.ykeys.length - 1; _ref <= 0 ? _i <= 0 : _i >= 0; i = _ref <= 0 ? ++_i : --_i) { this._drawLineFor(i); } _results = []; for (i = _j = _ref1 = this.options.ykeys.length - 1; _ref1 <= 0 ? _j <= 0 : _j >= 0; i = _ref1 <= 0 ? ++_j : --_j) { _results.push(this._drawPointFor(i)); } return _results; }; Line.prototype._drawPointFor = function(index) { var circle, row, _i, _len, _ref, _results; this.seriesPoints[index] = []; _ref = this.data; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { row = _ref[_i]; circle = null; if (row._y[index] != null) { circle = this.drawLinePoint(row._x, row._y[index], this.colorFor(row, index, 'point'), index); } _results.push(this.seriesPoints[index].push(circle)); } return _results; }; Line.prototype._drawLineFor = function(index) { var path; path = this.paths[index]; if (path !== null) { return this.drawLinePath(path, this.colorFor(null, index, 'line'), index); } }; Line.createPath = function(coords, smooth, bottom) { var coord, g, grads, i, ix, lg, path, prevCoord, x1, x2, y1, y2, _i, _len; path = ""; if (smooth) { grads = Morris.Line.gradients(coords); } prevCoord = { y: null }; for (i = _i = 0, _len = coords.length; _i < _len; i = ++_i) { coord = coords[i]; if (coord.y != null) { if (prevCoord.y != null) { if (smooth) { g = grads[i]; lg = grads[i - 1]; ix = (coord.x - prevCoord.x) / 4; x1 = prevCoord.x + ix; y1 = Math.min(bottom, prevCoord.y + ix * lg); x2 = coord.x - ix; y2 = Math.min(bottom, coord.y - ix * g); path += "C" + x1 + "," + y1 + "," + x2 + "," + y2 + "," + coord.x + "," + coord.y; } else { path += "L" + coord.x + "," + coord.y; } } else { if (!smooth || (grads[i] != null)) { path += "M" + coord.x + "," + coord.y; } } } prevCoord = coord; } return path; }; Line.gradients = function(coords) { var coord, grad, i, nextCoord, prevCoord, _i, _len, _results; grad = function(a, b) { return (a.y - b.y) / (a.x - b.x); }; _results = []; for (i = _i = 0, _len = coords.length; _i < _len; i = ++_i) { coord = coords[i]; if (coord.y != null) { nextCoord = coords[i + 1] || { y: null }; prevCoord = coords[i - 1] || { y: null }; if ((prevCoord.y != null) && (nextCoord.y != null)) { _results.push(grad(prevCoord, nextCoord)); } else if (prevCoord.y != null) { _results.push(grad(prevCoord, coord)); } else if (nextCoord.y != null) { _results.push(grad(coord, nextCoord)); } else { _results.push(null); } } else { _results.push(null); } } return _results; }; Line.prototype.hilight = function(index) { var i, _i, _j, _ref, _ref1; if (this.prevHilight !== null && this.prevHilight !== index) { for (i = _i = 0, _ref = this.seriesPoints.length - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) { if (this.seriesPoints[i][this.prevHilight]) { this.seriesPoints[i][this.prevHilight].animate(this.pointShrinkSeries(i)); } } } if (index !== null && this.prevHilight !== index) { for (i = _j = 0, _ref1 = this.seriesPoints.length - 1; 0 <= _ref1 ? _j <= _ref1 : _j >= _ref1; i = 0 <= _ref1 ? ++_j : --_j) { if (this.seriesPoints[i][index]) { this.seriesPoints[i][index].animate(this.pointGrowSeries(i)); } } } return this.prevHilight = index; }; Line.prototype.colorFor = function(row, sidx, type) { if (typeof this.options.lineColors === 'function') { return this.options.lineColors.call(this, row, sidx, type); } else if (type === 'point') { return this.options.pointFillColors[sidx % this.options.pointFillColors.length] || this.options.lineColors[sidx % this.options.lineColors.length]; } else { return this.options.lineColors[sidx % this.options.lineColors.length]; } }; Line.prototype.drawXAxisLabel = function(xPos, yPos, text) { return this.raphael.text(xPos, yPos, text).attr('font-size', this.options.gridTextSize).attr('font-family', this.options.gridTextFamily).attr('font-weight', this.options.gridTextWeight).attr('fill', this.options.gridTextColor); }; Line.prototype.drawLinePath = function(path, lineColor, lineIndex) { return this.raphael.path(path).attr('stroke', lineColor).attr('stroke-width', this.lineWidthForSeries(lineIndex)); }; Line.prototype.drawLinePoint = function(xPos, yPos, pointColor, lineIndex) { return this.raphael.circle(xPos, yPos, this.pointSizeForSeries(lineIndex)).attr('fill', pointColor).attr('stroke-width', this.pointStrokeWidthForSeries(lineIndex)).attr('stroke', this.pointStrokeColorForSeries(lineIndex)); }; Line.prototype.pointStrokeWidthForSeries = function(index) { return this.options.pointStrokeWidths[index % this.options.pointStrokeWidths.length]; }; Line.prototype.pointStrokeColorForSeries = function(index) { return this.options.pointStrokeColors[index % this.options.pointStrokeColors.length]; }; Line.prototype.lineWidthForSeries = function(index) { if (this.options.lineWidth instanceof Array) { return this.options.lineWidth[index % this.options.lineWidth.length]; } else { return this.options.lineWidth; } }; Line.prototype.pointSizeForSeries = function(index) { if (this.options.pointSize instanceof Array) { return this.options.pointSize[index % this.options.pointSize.length]; } else { return this.options.pointSize; } }; Line.prototype.pointGrowSeries = function(index) { return Raphael.animation({ r: this.pointSizeForSeries(index) + 3 }, 25, 'linear'); }; Line.prototype.pointShrinkSeries = function(index) { return Raphael.animation({ r: this.pointSizeForSeries(index) }, 25, 'linear'); }; return Line; })(Morris.Grid); Morris.labelSeries = function(dmin, dmax, pxwidth, specName, xLabelFormat) { var d, d0, ddensity, name, ret, s, spec, t, _i, _len, _ref; ddensity = 200 * (dmax - dmin) / pxwidth; d0 = new Date(dmin); spec = Morris.LABEL_SPECS[specName]; if (spec === void 0) { _ref = Morris.AUTO_LABEL_ORDER; for (_i = 0, _len = _ref.length; _i < _len; _i++) { name = _ref[_i]; s = Morris.LABEL_SPECS[name]; if (ddensity >= s.span) { spec = s; break; } } } if (spec === void 0) { spec = Morris.LABEL_SPECS["second"]; } if (xLabelFormat) { spec = $.extend({}, spec, { fmt: xLabelFormat }); } d = spec.start(d0); ret = []; while ((t = d.getTime()) <= dmax) { if (t >= dmin) { ret.push([spec.fmt(d), t]); } spec.incr(d); } return ret; }; minutesSpecHelper = function(interval) { return { span: interval * 60 * 1000, start: function(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()); }, fmt: function(d) { return "" + (Morris.pad2(d.getHours())) + ":" + (Morris.pad2(d.getMinutes())); }, incr: function(d) { return d.setUTCMinutes(d.getUTCMinutes() + interval); } }; }; secondsSpecHelper = function(interval) { return { span: interval * 1000, start: function(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes()); }, fmt: function(d) { return "" + (Morris.pad2(d.getHours())) + ":" + (Morris.pad2(d.getMinutes())) + ":" + (Morris.pad2(d.getSeconds())); }, incr: function(d) { return d.setUTCSeconds(d.getUTCSeconds() + interval); } }; }; Morris.LABEL_SPECS = { "decade": { span: 172800000000, start: function(d) { return new Date(d.getFullYear() - d.getFullYear() % 10, 0, 1); }, fmt: function(d) { return "" + (d.getFullYear()); }, incr: function(d) { return d.setFullYear(d.getFullYear() + 10); } }, "year": { span: 17280000000, start: function(d) { return new Date(d.getFullYear(), 0, 1); }, fmt: function(d) { return "" + (d.getFullYear()); }, incr: function(d) { return d.setFullYear(d.getFullYear() + 1); } }, "month": { span: 2419200000, start: function(d) { return new Date(d.getFullYear(), d.getMonth(), 1); }, fmt: function(d) { return "" + (d.getFullYear()) + "-" + (Morris.pad2(d.getMonth() + 1)); }, incr: function(d) { return d.setMonth(d.getMonth() + 1); } }, "week": { span: 604800000, start: function(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate()); }, fmt: function(d) { return "" + (d.getFullYear()) + "-" + (Morris.pad2(d.getMonth() + 1)) + "-" + (Morris.pad2(d.getDate())); }, incr: function(d) { return d.setDate(d.getDate() + 7); } }, "day": { span: 86400000, start: function(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate()); }, fmt: function(d) { return "" + (d.getFullYear()) + "-" + (Morris.pad2(d.getMonth() + 1)) + "-" + (Morris.pad2(d.getDate())); }, incr: function(d) { return d.setDate(d.getDate() + 1); } }, "hour": minutesSpecHelper(60), "30min": minutesSpecHelper(30), "15min": minutesSpecHelper(15), "10min": minutesSpecHelper(10), "5min": minutesSpecHelper(5), "minute": minutesSpecHelper(1), "30sec": secondsSpecHelper(30), "15sec": secondsSpecHelper(15), "10sec": secondsSpecHelper(10), "5sec": secondsSpecHelper(5), "second": secondsSpecHelper(1) }; Morris.AUTO_LABEL_ORDER = ["decade", "year", "month", "week", "day", "hour", "30min", "15min", "10min", "5min", "minute", "30sec", "15sec", "10sec", "5sec", "second"]; Morris.Area = (function(_super) { var areaDefaults; __extends(Area, _super); areaDefaults = { fillOpacity: 'auto', behaveLikeLine: false }; function Area(options) { var areaOptions; if (!(this instanceof Morris.Area)) { return new Morris.Area(options); } areaOptions = $.extend({}, areaDefaults, options); this.cumulative = !areaOptions.behaveLikeLine; if (areaOptions.fillOpacity === 'auto') { areaOptions.fillOpacity = areaOptions.behaveLikeLine ? .8 : 1; } Area.__super__.constructor.call(this, areaOptions); } Area.prototype.calcPoints = function() { var row, total, y, _i, _len, _ref, _results; _ref = this.data; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { row = _ref[_i]; row._x = this.transX(row.x); total = 0; row._y = (function() { var _j, _len1, _ref1, _results1; _ref1 = row.y; _results1 = []; for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { y = _ref1[_j]; if (this.options.behaveLikeLine) { _results1.push(this.transY(y)); } else { total += y || 0; _results1.push(this.transY(total)); } } return _results1; }).call(this); _results.push(row._ymax = Math.max.apply(Math, row._y)); } return _results; }; Area.prototype.drawSeries = function() { var i, range, _i, _j, _k, _len, _ref, _ref1, _results, _results1, _results2; this.seriesPoints = []; if (this.options.behaveLikeLine) { range = (function() { _results = []; for (var _i = 0, _ref = this.options.ykeys.length - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; 0 <= _ref ? _i++ : _i--){ _results.push(_i); } return _results; }).apply(this); } else { range = (function() { _results1 = []; for (var _j = _ref1 = this.options.ykeys.length - 1; _ref1 <= 0 ? _j <= 0 : _j >= 0; _ref1 <= 0 ? _j++ : _j--){ _results1.push(_j); } return _results1; }).apply(this); } _results2 = []; for (_k = 0, _len = range.length; _k < _len; _k++) { i = range[_k]; this._drawFillFor(i); this._drawLineFor(i); _results2.push(this._drawPointFor(i)); } return _results2; }; Area.prototype._drawFillFor = function(index) { var path; path = this.paths[index]; if (path !== null) { path = path + ("L" + (this.transX(this.xmax)) + "," + this.bottom + "L" + (this.transX(this.xmin)) + "," + this.bottom + "Z"); return this.drawFilledPath(path, this.fillForSeries(index)); } }; Area.prototype.fillForSeries = function(i) { var color; color = Raphael.rgb2hsl(this.colorFor(this.data[i], i, 'line')); return Raphael.hsl(color.h, this.options.behaveLikeLine ? color.s * 0.9 : color.s * 0.75, Math.min(0.98, this.options.behaveLikeLine ? color.l * 1.2 : color.l * 1.25)); }; Area.prototype.drawFilledPath = function(path, fill) { return this.raphael.path(path).attr('fill', fill).attr('fill-opacity', this.options.fillOpacity).attr('stroke', 'none'); }; return Area; })(Morris.Line); Morris.Bar = (function(_super) { __extends(Bar, _super); function Bar(options) { this.onHoverOut = __bind(this.onHoverOut, this); this.onHoverMove = __bind(this.onHoverMove, this); this.onGridClick = __bind(this.onGridClick, this); if (!(this instanceof Morris.Bar)) { return new Morris.Bar(options); } Bar.__super__.constructor.call(this, $.extend({}, options, { parseTime: false })); } Bar.prototype.init = function() { this.cumulative = this.options.stacked; if (this.options.hideHover !== 'always') { this.hover = new Morris.Hover({ parent: this.el }); this.on('hovermove', this.onHoverMove); this.on('hoverout', this.onHoverOut); return this.on('gridclick', this.onGridClick); } }; Bar.prototype.defaults = { barSizeRatio: 0.75, barGap: 3, barColors: ['#0b62a4', '#7a92a3', '#4da74d', '#afd8f8', '#edc240', '#cb4b4b', '#9440ed'], barOpacity: 1.0, barRadius: [0, 0, 0, 0], xLabelMargin: 50 }; Bar.prototype.calc = function() { var _ref; this.calcBars(); if (this.options.hideHover === false) { return (_ref = this.hover).update.apply(_ref, this.hoverContentForRow(this.data.length - 1)); } }; Bar.prototype.calcBars = function() { var idx, row, y, _i, _len, _ref, _results; _ref = this.data; _results = []; for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) { row = _ref[idx]; row._x = this.left + this.width * (idx + 0.5) / this.data.length; _results.push(row._y = (function() { var _j, _len1, _ref1, _results1; _ref1 = row.y; _results1 = []; for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { y = _ref1[_j]; if (y != null) { _results1.push(this.transY(y)); } else { _results1.push(null); } } return _results1; }).call(this)); } return _results; }; Bar.prototype.draw = function() { var _ref; if ((_ref = this.options.axes) === true || _ref === 'both' || _ref === 'x') { this.drawXAxis(); } return this.drawSeries(); }; Bar.prototype.drawXAxis = function() { var i, label, labelBox, margin, offset, prevAngleMargin, prevLabelMargin, row, textBox, ypos, _i, _ref, _results; ypos = this.bottom + (this.options.xAxisLabelTopPadding || this.options.padding / 2); prevLabelMargin = null; prevAngleMargin = null; _results = []; for (i = _i = 0, _ref = this.data.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { row = this.data[this.data.length - 1 - i]; label = this.drawXAxisLabel(row._x, ypos, row.label); textBox = label.getBBox(); label.transform("r" + (-this.options.xLabelAngle)); labelBox = label.getBBox(); label.transform("t0," + (labelBox.height / 2) + "..."); if (this.options.xLabelAngle !== 0) { offset = -0.5 * textBox.width * Math.cos(this.options.xLabelAngle * Math.PI / 180.0); label.transform("t" + offset + ",0..."); } if (((prevLabelMargin == null) || prevLabelMargin >= labelBox.x + labelBox.width || (prevAngleMargin != null) && prevAngleMargin >= labelBox.x) && labelBox.x >= 0 && (labelBox.x + labelBox.width) < this.el.width()) { if (this.options.xLabelAngle !== 0) { margin = 1.25 * this.options.gridTextSize / Math.sin(this.options.xLabelAngle * Math.PI / 180.0); prevAngleMargin = labelBox.x - margin; } _results.push(prevLabelMargin = labelBox.x - this.options.xLabelMargin); } else { _results.push(label.remove()); } } return _results; }; Bar.prototype.drawSeries = function() { var barWidth, bottom, groupWidth, idx, lastTop, left, leftPadding, numBars, row, sidx, size, spaceLeft, top, ypos, zeroPos; groupWidth = this.width / this.options.data.length; numBars = this.options.stacked ? 1 : this.options.ykeys.length; barWidth = (groupWidth * this.options.barSizeRatio - this.options.barGap * (numBars - 1)) / numBars; if (this.options.barSize) { barWidth = Math.min(barWidth, this.options.barSize); } spaceLeft = groupWidth - barWidth * numBars - this.options.barGap * (numBars - 1); leftPadding = spaceLeft / 2; zeroPos = this.ymin <= 0 && this.ymax >= 0 ? this.transY(0) : null; return this.bars = (function() { var _i, _len, _ref, _results; _ref = this.data; _results = []; for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) { row = _ref[idx]; lastTop = 0; _results.push((function() { var _j, _len1, _ref1, _results1; _ref1 = row._y; _results1 = []; for (sidx = _j = 0, _len1 = _ref1.length; _j < _len1; sidx = ++_j) { ypos = _ref1[sidx]; if (ypos !== null) { if (zeroPos) { top = Math.min(ypos, zeroPos); bottom = Math.max(ypos, zeroPos); } else { top = ypos; bottom = this.bottom; } left = this.left + idx * groupWidth + leftPadding; if (!this.options.stacked) { left += sidx * (barWidth + this.options.barGap); } size = bottom - top; if (this.options.verticalGridCondition && this.options.verticalGridCondition(row.x)) { this.drawBar(this.left + idx * groupWidth, this.top, groupWidth, Math.abs(this.top - this.bottom), this.options.verticalGridColor, this.options.verticalGridOpacity, this.options.barRadius); } if (this.options.stacked) { top -= lastTop; } this.drawBar(left, top, barWidth, size, this.colorFor(row, sidx, 'bar'), this.options.barOpacity, this.options.barRadius); _results1.push(lastTop += size); } else { _results1.push(null); } } return _results1; }).call(this)); } return _results; }).call(this); }; Bar.prototype.colorFor = function(row, sidx, type) { var r, s; if (typeof this.options.barColors === 'function') { r = { x: row.x, y: row.y[sidx], label: row.label }; s = { index: sidx, key: this.options.ykeys[sidx], label: this.options.labels[sidx] }; return this.options.barColors.call(this, r, s, type); } else { return this.options.barColors[sidx % this.options.barColors.length]; } }; Bar.prototype.hitTest = function(x) { if (this.data.length === 0) { return null; } x = Math.max(Math.min(x, this.right), this.left); return Math.min(this.data.length - 1, Math.floor((x - this.left) / (this.width / this.data.length))); }; Bar.prototype.onGridClick = function(x, y) { var index; index = this.hitTest(x); return this.fire('click', index, this.data[index].src, x, y); }; Bar.prototype.onHoverMove = function(x, y) { var index, _ref; index = this.hitTest(x); return (_ref = this.hover).update.apply(_ref, this.hoverContentForRow(index)); }; Bar.prototype.onHoverOut = function() { if (this.options.hideHover !== false) { return this.hover.hide(); } }; Bar.prototype.hoverContentForRow = function(index) { var content, j, row, x, y, _i, _len, _ref; row = this.data[index]; content = "<div class='morris-hover-row-label'>" + row.label + "</div>"; _ref = row.y; for (j = _i = 0, _len = _ref.length; _i < _len; j = ++_i) { y = _ref[j]; content += "<div class='morris-hover-point' style='color: " + (this.colorFor(row, j, 'label')) + "'>\n " + this.options.labels[j] + ":\n " + (this.yLabelFormat(y)) + "\n</div>"; } if (typeof this.options.hoverCallback === 'function') { content = this.options.hoverCallback(index, this.options, content, row.src); } x = this.left + (index + 0.5) * this.width / this.data.length; return [content, x]; }; Bar.prototype.drawXAxisLabel = function(xPos, yPos, text) { var label; return label = this.raphael.text(xPos, yPos, text).attr('font-size', this.options.gridTextSize).attr('font-family', this.options.gridTextFamily).attr('font-weight', this.options.gridTextWeight).attr('fill', this.options.gridTextColor); }; Bar.prototype.drawBar = function(xPos, yPos, width, height, barColor, opacity, radiusArray) { var maxRadius, path; maxRadius = Math.max.apply(Math, radiusArray); if (maxRadius === 0 || maxRadius > height) { path = this.raphael.rect(xPos, yPos, width, height); } else { path = this.raphael.path(this.roundedRect(xPos, yPos, width, height, radiusArray)); } return path.attr('fill', barColor).attr('fill-opacity', opacity).attr('stroke', 'none'); }; Bar.prototype.roundedRect = function(x, y, w, h, r) { if (r == null) { r = [0, 0, 0, 0]; } return ["M", x, r[0] + y, "Q", x, y, x + r[0], y, "L", x + w - r[1], y, "Q", x + w, y, x + w, y + r[1], "L", x + w, y + h - r[2], "Q", x + w, y + h, x + w - r[2], y + h, "L", x + r[3], y + h, "Q", x, y + h, x, y + h - r[3], "Z"]; }; return Bar; })(Morris.Grid); Morris.Donut = (function(_super) { __extends(Donut, _super); Donut.prototype.defaults = { colors: ['#0B62A4', '#3980B5', '#679DC6', '#95BBD7', '#B0CCE1', '#095791', '#095085', '#083E67', '#052C48', '#042135'], backgroundColor: '#FFFFFF', labelColor: '#000000', formatter: Morris.commas, resize: false }; function Donut(options) { this.resizeHandler = __bind(this.resizeHandler, this); this.select = __bind(this.select, this); this.click = __bind(this.click, this); var _this = this; if (!(this instanceof Morris.Donut)) { return new Morris.Donut(options); } this.options = $.extend({}, this.defaults, options); if (typeof options.element === 'string') { this.el = $(document.getElementById(options.element)); } else { this.el = $(options.element); } if (this.el === null || this.el.length === 0) { throw new Error("Graph placeholder not found."); } if (options.data === void 0 || options.data.length === 0) { return; } this.raphael = new Raphael(this.el[0]); if (this.options.resize) { $(window).bind('resize', function(evt) { if (_this.timeoutId != null) { window.clearTimeout(_this.timeoutId); } return _this.timeoutId = window.setTimeout(_this.resizeHandler, 100); }); } this.setData(options.data); } Donut.prototype.redraw = function() { var C, cx, cy, i, idx, last, max_value, min, next, seg, total, value, w, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _results; this.raphael.clear(); cx = this.el.width() / 2; cy = this.el.height() / 2; w = (Math.min(cx, cy) - 10) / 3; total = 0; _ref = this.values; for (_i = 0, _len = _ref.length; _i < _len; _i++) { value = _ref[_i]; total += value; } min = 5 / (2 * w); C = 1.9999 * Math.PI - min * this.data.length; last = 0; idx = 0; this.segments = []; _ref1 = this.values; for (i = _j = 0, _len1 = _ref1.length; _j < _len1; i = ++_j) { value = _ref1[i]; next = last + min + C * (value / total); seg = new Morris.DonutSegment(cx, cy, w * 2, w, last, next, this.data[i].color || this.options.colors[idx % this.options.colors.length], this.options.backgroundColor, idx, this.raphael); seg.render(); this.segments.push(seg); seg.on('hover', this.select); seg.on('click', this.click); last = next; idx += 1; } this.text1 = this.drawEmptyDonutLabel(cx, cy - 10, this.options.labelColor, 15, 800); this.text2 = this.drawEmptyDonutLabel(cx, cy + 10, this.options.labelColor, 14); max_value = Math.max.apply(Math, this.values); idx = 0; _ref2 = this.values; _results = []; for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { value = _ref2[_k]; if (value === max_value) { this.select(idx); break; } _results.push(idx += 1); } return _results; }; Donut.prototype.setData = function(data) { var row; this.data = data; this.values = (function() { var _i, _len, _ref, _results; _ref = this.data; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { row = _ref[_i]; _results.push(parseFloat(row.value)); } return _results; }).call(this); return this.redraw(); }; Donut.prototype.click = function(idx) { return this.fire('click', idx, this.data[idx]); }; Donut.prototype.select = function(idx) { var row, s, segment, _i, _len, _ref; _ref = this.segments; for (_i = 0, _len = _ref.length; _i < _len; _i++) { s = _ref[_i]; s.deselect(); } segment = this.segments[idx]; segment.select(); row = this.data[idx]; return this.setLabels(row.label, this.options.formatter(row.value, row)); }; Donut.prototype.setLabels = function(label1, label2) { var inner, maxHeightBottom, maxHeightTop, maxWidth, text1bbox, text1scale, text2bbox, text2scale; inner = (Math.min(this.el.width() / 2, this.el.height() / 2) - 10) * 2 / 3; maxWidth = 1.8 * inner; maxHeightTop = inner / 2; maxHeightBottom = inner / 3; this.text1.attr({ text: label1, transform: '' }); text1bbox = this.text1.getBBox(); text1scale = Math.min(maxWidth / text1bbox.width, maxHeightTop / text1bbox.height); this.text1.attr({ transform: "S" + text1scale + "," + text1scale + "," + (text1bbox.x + text1bbox.width / 2) + "," + (text1bbox.y + text1bbox.height) }); this.text2.attr({ text: label2, transform: '' }); text2bbox = this.text2.getBBox(); text2scale = Math.min(maxWidth / text2bbox.width, maxHeightBottom / text2bbox.height); return this.text2.attr({ transform: "S" + text2scale + "," + text2scale + "," + (text2bbox.x + text2bbox.width / 2) + "," + text2bbox.y }); }; Donut.prototype.drawEmptyDonutLabel = function(xPos, yPos, color, fontSize, fontWeight) { var text; text = this.raphael.text(xPos, yPos, '').attr('font-size', fontSize).attr('fill', color); if (fontWeight != null) { text.attr('font-weight', fontWeight); } return text; }; Donut.prototype.resizeHandler = function() { this.timeoutId = null; this.raphael.setSize(this.el.width(), this.el.height()); return this.redraw(); }; return Donut; })(Morris.EventEmitter); Morris.DonutSegment = (function(_super) { __extends(DonutSegment, _super); function DonutSegment(cx, cy, inner, outer, p0, p1, color, backgroundColor, index, raphael) { this.cx = cx; this.cy = cy; this.inner = inner; this.outer = outer; this.color = color; this.backgroundColor = backgroundColor; this.index = index; this.raphael = raphael; this.deselect = __bind(this.deselect, this); this.select = __bind(this.select, this); this.sin_p0 = Math.sin(p0); this.cos_p0 = Math.cos(p0); this.sin_p1 = Math.sin(p1); this.cos_p1 = Math.cos(p1); this.is_long = (p1 - p0) > Math.PI ? 1 : 0; this.path = this.calcSegment(this.inner + 3, this.inner + this.outer - 5); this.selectedPath = this.calcSegment(this.inner + 3, this.inner + this.outer); this.hilight = this.calcArc(this.inner); } DonutSegment.prototype.calcArcPoints = function(r) { return [this.cx + r * this.sin_p0, this.cy + r * this.cos_p0, this.cx + r * this.sin_p1, this.cy + r * this.cos_p1]; }; DonutSegment.prototype.calcSegment = function(r1, r2) { var ix0, ix1, iy0, iy1, ox0, ox1, oy0, oy1, _ref, _ref1; _ref = this.calcArcPoints(r1), ix0 = _ref[0], iy0 = _ref[1], ix1 = _ref[2], iy1 = _ref[3]; _ref1 = this.calcArcPoints(r2), ox0 = _ref1[0], oy0 = _ref1[1], ox1 = _ref1[2], oy1 = _ref1[3]; return ("M" + ix0 + "," + iy0) + ("A" + r1 + "," + r1 + ",0," + this.is_long + ",0," + ix1 + "," + iy1) + ("L" + ox1 + "," + oy1) + ("A" + r2 + "," + r2 + ",0," + this.is_long + ",1," + ox0 + "," + oy0) + "Z"; }; DonutSegment.prototype.calcArc = function(r) { var ix0, ix1, iy0, iy1, _ref; _ref = this.calcArcPoints(r), ix0 = _ref[0], iy0 = _ref[1], ix1 = _ref[2], iy1 = _ref[3]; return ("M" + ix0 + "," + iy0) + ("A" + r + "," + r + ",0," + this.is_long + ",0," + ix1 + "," + iy1); }; DonutSegment.prototype.render = function() { var _this = this; this.arc = this.drawDonutArc(this.hilight, this.color); return this.seg = this.drawDonutSegment(this.path, this.color, this.backgroundColor, function() { return _this.fire('hover', _this.index); }, function() { return _this.fire('click', _this.index); }); }; DonutSegment.prototype.drawDonutArc = function(path, color) { return this.raphael.path(path).attr({ stroke: color, 'stroke-width': 2, opacity: 0 }); }; DonutSegment.prototype.drawDonutSegment = function(path, fillColor, strokeColor, hoverFunction, clickFunction) { return this.raphael.path(path).attr({ fill: fillColor, stroke: strokeColor, 'stroke-width': 3 }).hover(hoverFunction).click(clickFunction); }; DonutSegment.prototype.select = function() { if (!this.selected) { this.seg.animate({ path: this.selectedPath }, 150, '<>'); this.arc.animate({ opacity: 1 }, 150, '<>'); return this.selected = true; } }; DonutSegment.prototype.deselect = function() { if (this.selected) { this.seg.animate({ path: this.path }, 150, '<>'); this.arc.animate({ opacity: 0 }, 150, '<>'); return this.selected = false; } }; return DonutSegment; })(Morris.EventEmitter); }).call(this); morris/morris.css 0000644 00000001106 15030376016 0010103 0 ustar 00 /* Morris charts */ .morris-hover { position: absolute; z-index: 1000; } .morris-hover.morris-default-style { border-radius: 10px; padding: 6px; color: #666; background: rgba(255, 255, 255, 0.8); border: solid 2px rgba(230, 230, 230, 0.8); font-family: sans-serif; font-size: 12px; text-align: center; } .morris-hover.morris-default-style .morris-hover-row-label { font-weight: bold; margin: 0.25em 0; } .morris-hover.morris-default-style .morris-hover-point { white-space: nowrap; margin: 0.1em 0; } morris/morris-demo.js 0000644 00000020367 15030376016 0010663 0 ustar 00 /* Morris color bar */ $(function() { "use strict"; Morris.Bar({ element: 'color-bar', data: [{ x: '2011 Q1', y: 0 }, { x: '2011 Q2', y: 1 }, { x: '2011 Q3', y: 2 }, { x: '2011 Q4', y: 3 }, { x: '2012 Q1', y: 4 }, { x: '2012 Q2', y: 5 }, { x: '2012 Q3', y: 6 }, { x: '2012 Q4', y: 7 }, { x: '2013 Q1', y: 8 }], xkey: 'x', ykeys: ['y'], labels: ['Y'], barColors: function(row, series, type) { if (type === 'bar') { var red = Math.ceil(255 * row.y / this.ymax); return 'rgb(' + red + ',155,22)'; } else { return '#000'; } } }); }); /* Morris labels bar */ $(function() { "use strict"; var day_data = [{ "period": "2012-10-01", "licensed": 3407, "sorned": 660 }, { "period": "2012-09-30", "licensed": 3351, "sorned": 629 }, { "period": "2012-09-29", "licensed": 3269, "sorned": 618 }, { "period": "2012-09-20", "licensed": 3246, "sorned": 661 }, { "period": "2012-09-19", "licensed": 3257, "sorned": 667 }, { "period": "2012-09-18", "licensed": 3248, "sorned": 627 }, { "period": "2012-09-17", "licensed": 3171, "sorned": 660 }, { "period": "2012-09-16", "licensed": 3171, "sorned": 676 }, { "period": "2012-09-15", "licensed": 3201, "sorned": 656 }, { "period": "2012-09-10", "licensed": 3215, "sorned": 622 }]; Morris.Bar({ element: 'labels-bar', data: day_data, xkey: 'period', ykeys: ['licensed', 'sorned'], labels: ['Licensed', 'SORN'], xLabelAngle: 60 }); }); /* Morris stacked bars */ $(function() { "use strict"; Morris.Bar({ element: 'stacked-bars', data: [{ x: '2011 Q1', y: 3, z: 2, a: 3 }, { x: '2011 Q2', y: 2, z: null, a: 1 }, { x: '2011 Q3', y: 0, z: 2, a: 4 }, { x: '2011 Q4', y: 2, z: 4, a: 3 }], xkey: 'x', ykeys: ['y', 'z', 'a'], labels: ['Y', 'Z', 'A'], stacked: true }); }); /* Morris donut */ $(function() { "use strict"; Morris.Donut({ element: 'donut', backgroundColor: '#fff', labelColor: '#ccc', colors: [ '#4fb2ff', '#929292', '#67C69D', '#ff9393' ], data: [{ value: 70, label: 'foo', formatted: 'at least 70%' }, { value: 15, label: 'bar', formatted: 'approx. 15%' }, { value: 10, label: 'baz', formatted: 'approx. 10%' }, { value: 5, label: 'A really really long label', formatted: 'at most 5%' }], formatter: function(x, data) { return data.formatted; } }); }); /* Morris decimal data */ $(function() { "use strict"; var decimal_data = []; for (var x = 0; x <= 360; x += 10) { decimal_data.push({ x: x, y: 1.5 + 1.5 * Math.sin(Math.PI * x / 180).toFixed(4) }); } window.m = Morris.Line({ element: 'decimal-data', data: decimal_data, xkey: 'x', ykeys: ['y'], labels: ['sin(x)'], parseTime: false, hoverCallback: function(index, options, default_content) { var row = options.data[index]; return default_content.replace("sin(x)", "1.5 + 1.5 sin(" + row.x + ")"); }, xLabelMargin: 10, integerYLabels: true }); }); /* Morris daytime */ $(function() { "use strict"; Morris.Area({ element: 'daytime-bars', data: [{ x: '2013-03-30 22:00:00', y: 3, z: 3 }, { x: '2013-03-31 00:00:00', y: 2, z: 0 }, { x: '2013-03-31 02:00:00', y: 0, z: 2 }, { x: '2013-03-31 04:00:00', y: 4, z: 4 }], xkey: 'x', ykeys: ['y', 'z'], labels: ['Y', 'Z'] }); }); /* Author: */ $(function() { // data stolen from http://howmanyleft.co.uk/vehicle/jaguar_'e'_type var tax_data = [{ "period": "2011 Q3", "licensed": 3407, "sorned": 660 }, { "period": "2011 Q2", "licensed": 3351, "sorned": 629 }, { "period": "2011 Q1", "licensed": 3269, "sorned": 618 }, { "period": "2010 Q4", "licensed": 3246, "sorned": 661 }, { "period": "2009 Q4", "licensed": 3171, "sorned": 676 }, { "period": "2008 Q4", "licensed": 3155, "sorned": 681 }, { "period": "2007 Q4", "licensed": 3226, "sorned": 620 }, { "period": "2006 Q4", "licensed": 3245, "sorned": null }, { "period": "2005 Q4", "licensed": 3289, "sorned": null }]; Morris.Line({ element: 'hero-graph', data: tax_data, xkey: 'period', ykeys: ['licensed', 'sorned'], labels: ['Licensed', 'Off the road'] }); Morris.Donut({ element: 'hero-donut', data: [{ label: 'Jam', value: 25 }, { label: 'Frosted', value: 40 }, { label: 'Custard', value: 25 }, { label: 'Sugar', value: 10 }], formatter: function(y) { return y + "%" } }); Morris.Area({ element: 'hero-area', data: [{ period: '2010 Q1', iphone: 2666, ipad: null, itouch: 2647 }, { period: '2010 Q2', iphone: 2778, ipad: 2294, itouch: 2441 }, { period: '2010 Q3', iphone: 4912, ipad: 1969, itouch: 2501 }, { period: '2010 Q4', iphone: 3767, ipad: 3597, itouch: 5689 }, { period: '2011 Q1', iphone: 6810, ipad: 1914, itouch: 2293 }, { period: '2011 Q2', iphone: 5670, ipad: 4293, itouch: 1881 }, { period: '2011 Q3', iphone: 4820, ipad: 3795, itouch: 1588 }, { period: '2011 Q4', iphone: 15073, ipad: 5967, itouch: 5175 }, { period: '2012 Q1', iphone: 10687, ipad: 4460, itouch: 2028 }, { period: '2012 Q2', iphone: 8432, ipad: 5713, itouch: 1791 }], xkey: 'period', ykeys: ['iphone', 'ipad', 'itouch'], labels: ['iPhone', 'iPad', 'iPod Touch'], pointSize: 2, hideHover: 'auto' }); Morris.Bar({ element: 'hero-bar', data: [{ device: 'iPhone', geekbench: 136 }, { device: 'iPhone 3G', geekbench: 137 }, { device: 'iPhone 3GS', geekbench: 275 }, { device: 'iPhone 4', geekbench: 380 }, { device: 'iPhone 4S', geekbench: 655 }, { device: 'iPhone 5', geekbench: 1571 }], xkey: 'device', ykeys: ['geekbench'], labels: ['Geekbench'], barRatio: 0.4, xLabelAngle: 35, hideHover: 'auto' }); }); sparklines/sparklines-demo.js 0000644 00000016116 15030376016 0012360 0 ustar 00 /* Sparklines charts */ $(function() { "use strict"; $('.sparkbar').sparkline('html', { type: 'bar', disableHiddenCheck: false, width: '36px', height: '36px' }); }); /* Sparklines */ $(function() { "use strict"; $('.bar-sparkline-btn').sparkline([ [3, 5], [4, 7], [2, 5], [3, 5], [4, 7], [4, 7], [5, 7], [2, 7], [3, 5] ], { type: 'bar', height: '53px', barWidth: '5px', barSpacing: '2px' }); }); $(function() { "use strict"; $('.bar-sparkline-btn-2').sparkline([ [3, 5], [4, 7], [2, 5], [3, 5], [4, 7], [4, 7], [5, 7], [2, 7], [3, 5] ], { type: 'bar', height: '40px', barWidth: '3px', barSpacing: '2px' }); }); $(function() { "use strict"; $('.bar-sparkline').sparkline([ [4, 8], [2, 7], [2, 6], [2, 7], [3, 5], [2, 7], [2, 6], [2, 7], [3, 5], [4, 7], [2, 5], [3, 5], [4, 7], [4, 7], [5, 7], [4, 8], [2, 7], [2, 6], [2, 7], [3, 5] ], { type: 'bar', height: '35px', barWidth: '5px', barSpacing: '2px' }); }); $(function() { "use strict"; $('.bar-sparkline-2').sparkline('html', { type: 'bar', barColor: 'black', height: '35px', barWidth: '5px', barSpacing: '2px' }); }); $(function() { "use strict"; $('.tristate-sparkline').sparkline('html', { type: 'tristate', barColor: 'black', height: '35px', barWidth: '5px', barSpacing: '2px' }); }); $(function() { "use strict"; $('.discrete-sparkline').sparkline('html', { type: 'discrete', barColor: 'black', height: '45px', barSpacing: '4px' }); }); $(function() { "use strict"; $('.pie-sparkline').sparkline('html', { type: 'pie', barColor: 'black', height: '45px', width: '45px' }); }); $(function() { "use strict"; $(".pie-sparkline-alt").sparkline('html', { type: 'pie', width: '100', height: '100', sliceColors: ['#EFEFEF', '#5BCCF6', '#FA7753'], borderWidth: 0 }); }); $(function() { "use strict"; var myvalues = [10, 8, 5, 7, 4, 4, 1]; $('.dynamic-sparkline').sparkline(myvalues, { height: '35px', width: '135px' }); }); $(function() { "use strict"; var myvalues = [10, 8, 5, 7, 4, 4, 1]; $('.dynamic-sparkline-5').sparkline(myvalues, { height: '57px', width: '100px' }); }); $(function() { "use strict"; $('.tristate-sparkline-2').sparkline('html', { type: 'tristate', posBarColor: '#ec6a00', negBarColor: '#ffc98a', zeroBarColor: '#000000', height: '35px', barWidth: '5px', barSpacing: '2px' }); }); $(function() { "use strict"; $(".infobox-sparkline").sparkline([ [3, 5], [4, 7], [2, 5], [3, 5], [4, 7], [4, 7], [5, 7], [2, 7], [3, 5] ], { type: 'bar', height: '53', barWidth: 5, barSpacing: 2, zeroAxis: false, barColor: '#ccc', negBarColor: '#ddd', zeroColor: '#ccc', stackedBarColor: ['#871010', '#ffebeb'] }); }); $(function() { "use strict"; $(".infobox-sparkline-2").sparkline([ [3, 5], [4, 7], [2, 5], [3, 5], [4, 7], [4, 7], [5, 7], [2, 7], [3, 5] ], { type: 'bar', height: '53', barWidth: 5, barSpacing: 2, zeroAxis: false, barColor: '#ccc', negBarColor: '#ddd', zeroColor: '#ccc', stackedBarColor: ['#000000', '#cccccc'] }); }); $(function() { "use strict"; $(".infobox-sparkline-pie").sparkline([1.5, 2.5, 2], { type: 'pie', width: '57', height: '57', sliceColors: ['#0d4f26', '#00712b', '#2eee76'], offset: 0, borderWidth: 0, borderColor: '#000000' }); }); $(function() { "use strict"; $(".infobox-sparkline-tri").sparkline([1, 1, 0, 1, -1, -1, 1, -1, 0, 0, 2, 1], { type: 'tristate', height: '53', posBarColor: '#1bb1fc', negBarColor: '#3d57ed', zeroBarColor: '#000000', barWidth: 5 }); }); $(function() { "use strict"; $(".sprk-1").sparkline('html', { type: 'line', width: '50%', height: '65', lineColor: '#b2b2b2', fillColor: '#ffffff', lineWidth: 1, spotColor: '#0065ff', minSpotColor: '#0065ff', maxSpotColor: '#0065ff', spotRadius: 4 }); }); $(function() { "use strict"; $(".sparkline-big").sparkline('html', { type: 'line', width: '85%', height: '80', highlightLineColor: '#ffffff', lineColor: '#ffffff', fillColor: 'transparent', lineWidth: 1, spotColor: '#ffffff', minSpotColor: '#ffffff', maxSpotColor: '#ffffff', highlightSpotColor: '#000000', spotRadius: 4 }); }); $(function() { "use strict"; $(".sparkline-big-alt").sparkline('html', { type: 'line', width: '90%', height: '110', highlightLineColor: '#accfff', lineColor: 'rgba(0,0,0,0.1)', fillColor: '#fcfeff', lineWidth: 1, spotColor: 'transparent', minSpotColor: 'transparent', maxSpotColor: 'transparent', highlightSpotColor: '#65a6ff', spotRadius: 6 }); }); $(function() { "use strict"; $(".sparkline-bar-big").sparkline('html', { type: 'bar', width: '85%', height: '90', barWidth: 6, barSpacing: 2, zeroAxis: false, barColor: '#ffffff', negBarColor: '#ffffff' }); }); $(function() { "use strict"; $(".sparkline-bar-big-color").sparkline('html', { type: 'bar', height: '90', width: '85%', barWidth: 6, barSpacing: 2, zeroAxis: false, barColor: '#9CD159', negBarColor: '#9CD159' }); }); $(function() { "use strict"; $(".sparkline-bar-big-color-2").sparkline([405, 450, 302, 405, 230, 311, 405, 342, 579, 405, 450, 302, 183, 579, 180, 311, 405, 342, 579, 405, 450, 302, 405, 230, 311, 405, 342, 579, 405, 450, 302, 405, 342, 432, 405, 450, 302, 183, 579, 180, 311, 405, 342, 579, 183, 579, 180, 311, 405, 342, 579, 405, 450, 302, 405, 230, 311, 405, 342, 579, 405, 450, 302, 405, 342, 432, 405, 450, 302, 183, 579, 180, 311, 405, 342, 579, 240, 180, 311, 450, 302, 370, 210], { type: 'bar', height: '88', width: '85%', barWidth: 6, barSpacing: 2, zeroAxis: false, barColor: '#9CD159', negBarColor: '#9CD159' }); }); sparklines/sparklines.js 0000644 00000405137 15030376016 0011443 0 ustar 00 /** * * jquery.sparkline.js * * v2.1.2 * (c) Splunk, Inc * Contact: Gareth Watts (gareth@splunk.com) * http://omnipotent.net/jquery.sparkline/ * * Generates inline sparkline charts from data supplied either to the method * or inline in HTML * * Compatible with Internet Explorer 6.0+ and modern browsers equipped with the canvas tag * (Firefox 2.0+, Safari, Opera, etc) * * License: New BSD License * * Copyright (c) 2012, Splunk Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of Splunk Inc nor the names of its contributors may * be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT * SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * * Usage: * $(selector).sparkline(values, options) * * If values is undefined or set to 'html' then the data values are read from the specified tag: * <p>Sparkline: <span class="sparkline">1,4,6,6,8,5,3,5</span></p> * $('.sparkline').sparkline(); * There must be no spaces in the enclosed data set * * Otherwise values must be an array of numbers or null values * <p>Sparkline: <span id="sparkline1">This text replaced if the browser is compatible</span></p> * $('#sparkline1').sparkline([1,4,6,6,8,5,3,5]) * $('#sparkline2').sparkline([1,4,6,null,null,5,3,5]) * * Values can also be specified in an HTML comment, or as a values attribute: * <p>Sparkline: <span class="sparkline"><!--1,4,6,6,8,5,3,5 --></span></p> * <p>Sparkline: <span class="sparkline" values="1,4,6,6,8,5,3,5"></span></p> * $('.sparkline').sparkline(); * * For line charts, x values can also be specified: * <p>Sparkline: <span class="sparkline">1:1,2.7:4,3.4:6,5:6,6:8,8.7:5,9:3,10:5</span></p> * $('#sparkline1').sparkline([ [1,1], [2.7,4], [3.4,6], [5,6], [6,8], [8.7,5], [9,3], [10,5] ]) * * By default, options should be passed in as teh second argument to the sparkline function: * $('.sparkline').sparkline([1,2,3,4], {type: 'bar'}) * * Options can also be set by passing them on the tag itself. This feature is disabled by default though * as there's a slight performance overhead: * $('.sparkline').sparkline([1,2,3,4], {enableTagOptions: true}) * <p>Sparkline: <span class="sparkline" sparkType="bar" sparkBarColor="red">loading</span></p> * Prefix all options supplied as tag attribute with "spark" (configurable by setting tagOptionPrefix) * * Supported options: * lineColor - Color of the line used for the chart * fillColor - Color used to fill in the chart - Set to '' or false for a transparent chart * width - Width of the chart - Defaults to 3 times the number of values in pixels * height - Height of the chart - Defaults to the height of the containing element * chartRangeMin - Specify the minimum value to use for the Y range of the chart - Defaults to the minimum value supplied * chartRangeMax - Specify the maximum value to use for the Y range of the chart - Defaults to the maximum value supplied * chartRangeClip - Clip out of range values to the max/min specified by chartRangeMin and chartRangeMax * chartRangeMinX - Specify the minimum value to use for the X range of the chart - Defaults to the minimum value supplied * chartRangeMaxX - Specify the maximum value to use for the X range of the chart - Defaults to the maximum value supplied * composite - If true then don't erase any existing chart attached to the tag, but draw * another chart over the top - Note that width and height are ignored if an * existing chart is detected. * tagValuesAttribute - Name of tag attribute to check for data values - Defaults to 'values' * enableTagOptions - Whether to check tags for sparkline options * tagOptionPrefix - Prefix used for options supplied as tag attributes - Defaults to 'spark' * disableHiddenCheck - If set to true, then the plugin will assume that charts will never be drawn into a * hidden dom element, avoding a browser reflow * disableInteraction - If set to true then all mouseover/click interaction behaviour will be disabled, * making the plugin perform much like it did in 1.x * disableTooltips - If set to true then tooltips will be disabled - Defaults to false (tooltips enabled) * disableHighlight - If set to true then highlighting of selected chart elements on mouseover will be disabled * defaults to false (highlights enabled) * highlightLighten - Factor to lighten/darken highlighted chart values by - Defaults to 1.4 for a 40% increase * tooltipContainer - Specify which DOM element the tooltip should be rendered into - defaults to document.body * tooltipClassname - Optional CSS classname to apply to tooltips - If not specified then a default style will be applied * tooltipOffsetX - How many pixels away from the mouse pointer to render the tooltip on the X axis * tooltipOffsetY - How many pixels away from the mouse pointer to render the tooltip on the r axis * tooltipFormatter - Optional callback that allows you to override the HTML displayed in the tooltip * callback is given arguments of (sparkline, options, fields) * tooltipChartTitle - If specified then the tooltip uses the string specified by this setting as a title * tooltipFormat - A format string or SPFormat object (or an array thereof for multiple entries) * to control the format of the tooltip * tooltipPrefix - A string to prepend to each field displayed in a tooltip * tooltipSuffix - A string to append to each field displayed in a tooltip * tooltipSkipNull - If true then null values will not have a tooltip displayed (defaults to true) * tooltipValueLookups - An object or range map to map field values to tooltip strings * (eg. to map -1 to "Lost", 0 to "Draw", and 1 to "Win") * numberFormatter - Optional callback for formatting numbers in tooltips * numberDigitGroupSep - Character to use for group separator in numbers "1,234" - Defaults to "," * numberDecimalMark - Character to use for the decimal point when formatting numbers - Defaults to "." * numberDigitGroupCount - Number of digits between group separator - Defaults to 3 * * There are 7 types of sparkline, selected by supplying a "type" option of 'line' (default), * 'bar', 'tristate', 'bullet', 'discrete', 'pie' or 'box' * line - Line chart. Options: * spotColor - Set to '' to not end each line in a circular spot * minSpotColor - If set, color of spot at minimum value * maxSpotColor - If set, color of spot at maximum value * spotRadius - Radius in pixels * lineWidth - Width of line in pixels * normalRangeMin * normalRangeMax - If set draws a filled horizontal bar between these two values marking the "normal" * or expected range of values * normalRangeColor - Color to use for the above bar * drawNormalOnTop - Draw the normal range above the chart fill color if true * defaultPixelsPerValue - Defaults to 3 pixels of width for each value in the chart * highlightSpotColor - The color to use for drawing a highlight spot on mouseover - Set to null to disable * highlightLineColor - The color to use for drawing a highlight line on mouseover - Set to null to disable * valueSpots - Specify which points to draw spots on, and in which color. Accepts a range map * * bar - Bar chart. Options: * barColor - Color of bars for postive values * negBarColor - Color of bars for negative values * zeroColor - Color of bars with zero values * nullColor - Color of bars with null values - Defaults to omitting the bar entirely * barWidth - Width of bars in pixels * colorMap - Optional mappnig of values to colors to override the *BarColor values above * can be an Array of values to control the color of individual bars or a range map * to specify colors for individual ranges of values * barSpacing - Gap between bars in pixels * zeroAxis - Centers the y-axis around zero if true * * tristate - Charts values of win (>0), lose (<0) or draw (=0) * posBarColor - Color of win values * negBarColor - Color of lose values * zeroBarColor - Color of draw values * barWidth - Width of bars in pixels * barSpacing - Gap between bars in pixels * colorMap - Optional mappnig of values to colors to override the *BarColor values above * can be an Array of values to control the color of individual bars or a range map * to specify colors for individual ranges of values * * discrete - Options: * lineHeight - Height of each line in pixels - Defaults to 30% of the graph height * thesholdValue - Values less than this value will be drawn using thresholdColor instead of lineColor * thresholdColor * * bullet - Values for bullet graphs msut be in the order: target, performance, range1, range2, range3, ... * options: * targetColor - The color of the vertical target marker * targetWidth - The width of the target marker in pixels * performanceColor - The color of the performance measure horizontal bar * rangeColors - Colors to use for each qualitative range background color * * pie - Pie chart. Options: * sliceColors - An array of colors to use for pie slices * offset - Angle in degrees to offset the first slice - Try -90 or +90 * borderWidth - Width of border to draw around the pie chart, in pixels - Defaults to 0 (no border) * borderColor - Color to use for the pie chart border - Defaults to #000 * * box - Box plot. Options: * raw - Set to true to supply pre-computed plot points as values * values should be: low_outlier, low_whisker, q1, median, q3, high_whisker, high_outlier * When set to false you can supply any number of values and the box plot will * be computed for you. Default is false. * showOutliers - Set to true (default) to display outliers as circles * outlierIQR - Interquartile range used to determine outliers. Default 1.5 * boxLineColor - Outline color of the box * boxFillColor - Fill color for the box * whiskerColor - Line color used for whiskers * outlierLineColor - Outline color of outlier circles * outlierFillColor - Fill color of the outlier circles * spotRadius - Radius of outlier circles * medianColor - Line color of the median line * target - Draw a target cross hair at the supplied value (default undefined) * * * * Examples: * $('#sparkline1').sparkline(myvalues, { lineColor: '#f00', fillColor: false }); * $('.barsparks').sparkline('html', { type:'bar', height:'40px', barWidth:5 }); * $('#tristate').sparkline([1,1,-1,1,0,0,-1], { type:'tristate' }): * $('#discrete').sparkline([1,3,4,5,5,3,4,5], { type:'discrete' }); * $('#bullet').sparkline([10,12,12,9,7], { type:'bullet' }); * $('#pie').sparkline([1,1,2], { type:'pie' }); */ /*jslint regexp: true, browser: true, jquery: true, white: true, nomen: false, plusplus: false, maxerr: 500, indent: 4 */ (function(document, Math, undefined) { // performance/minified-size optimization (function(factory) { if(typeof define === 'function' && define.amd) { define(['jquery'], factory); } else if (jQuery && !jQuery.fn.sparkline) { factory(jQuery); } } (function($) { 'use strict'; var UNSET_OPTION = {}, getDefaults, createClass, SPFormat, clipval, quartile, normalizeValue, normalizeValues, remove, isNumber, all, sum, addCSS, ensureArray, formatNumber, RangeMap, MouseHandler, Tooltip, barHighlightMixin, line, bar, tristate, discrete, bullet, pie, box, defaultStyles, initStyles, VShape, VCanvas_base, VCanvas_canvas, VCanvas_vml, pending, shapeCount = 0; /** * Default configuration settings */ getDefaults = function () { return { // Settings common to most/all chart types common: { type: 'line', lineColor: '#00f', fillColor: '#cdf', defaultPixelsPerValue: 3, width: 'auto', height: 'auto', composite: false, tagValuesAttribute: 'values', tagOptionsPrefix: 'spark', enableTagOptions: false, enableHighlight: true, highlightLighten: 1.4, tooltipSkipNull: true, tooltipPrefix: '', tooltipSuffix: '', disableHiddenCheck: false, numberFormatter: false, numberDigitGroupCount: 3, numberDigitGroupSep: ',', numberDecimalMark: '.', disableTooltips: false, disableInteraction: false }, // Defaults for line charts line: { spotColor: '#f80', highlightSpotColor: '#5f5', highlightLineColor: '#f22', spotRadius: 1.5, minSpotColor: '#f80', maxSpotColor: '#f80', lineWidth: 1, normalRangeMin: undefined, normalRangeMax: undefined, normalRangeColor: '#ccc', drawNormalOnTop: false, chartRangeMin: undefined, chartRangeMax: undefined, chartRangeMinX: undefined, chartRangeMaxX: undefined, tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{prefix}}{{y}}{{suffix}}') }, // Defaults for bar charts bar: { barColor: '#3366cc', negBarColor: '#f44', stackedBarColor: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00', '#dd4477', '#0099c6', '#990099'], zeroColor: undefined, nullColor: undefined, zeroAxis: true, barWidth: 4, barSpacing: 1, chartRangeMax: undefined, chartRangeMin: undefined, chartRangeClip: false, colorMap: undefined, tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{prefix}}{{value}}{{suffix}}') }, // Defaults for tristate charts tristate: { barWidth: 4, barSpacing: 1, posBarColor: '#6f6', negBarColor: '#f44', zeroBarColor: '#999', colorMap: {}, tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{value:map}}'), tooltipValueLookups: { map: { '-1': 'Loss', '0': 'Draw', '1': 'Win' } } }, // Defaults for discrete charts discrete: { lineHeight: 'auto', thresholdColor: undefined, thresholdValue: 0, chartRangeMax: undefined, chartRangeMin: undefined, chartRangeClip: false, tooltipFormat: new SPFormat('{{prefix}}{{value}}{{suffix}}') }, // Defaults for bullet charts bullet: { targetColor: '#f33', targetWidth: 3, // width of the target bar in pixels performanceColor: '#33f', rangeColors: ['#d3dafe', '#a8b6ff', '#7f94ff'], base: undefined, // set this to a number to change the base start number tooltipFormat: new SPFormat('{{fieldkey:fields}} - {{value}}'), tooltipValueLookups: { fields: {r: 'Range', p: 'Performance', t: 'Target'} } }, // Defaults for pie charts pie: { offset: 0, sliceColors: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00', '#dd4477', '#0099c6', '#990099'], borderWidth: 0, borderColor: '#000', tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{value}} ({{percent.1}}%)') }, // Defaults for box plots box: { raw: false, boxLineColor: '#000', boxFillColor: '#cdf', whiskerColor: '#000', outlierLineColor: '#333', outlierFillColor: '#fff', medianColor: '#f00', showOutliers: true, outlierIQR: 1.5, spotRadius: 1.5, target: undefined, targetColor: '#4a2', chartRangeMax: undefined, chartRangeMin: undefined, tooltipFormat: new SPFormat('{{field:fields}}: {{value}}'), tooltipFormatFieldlistKey: 'field', tooltipValueLookups: { fields: { lq: 'Lower Quartile', med: 'Median', uq: 'Upper Quartile', lo: 'Left Outlier', ro: 'Right Outlier', lw: 'Left Whisker', rw: 'Right Whisker'} } } }; }; // You can have tooltips use a css class other than jqstooltip by specifying tooltipClassname defaultStyles = '.jqstooltip { ' + 'position: absolute;' + 'left: 0px;' + 'top: 0px;' + 'visibility: hidden;' + 'background: rgb(0, 0, 0) transparent;' + 'background-color: rgba(0,0,0,0.6);' + 'filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);' + '-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";' + 'color: white;' + 'font: 10px arial, san serif;' + 'text-align: left;' + 'white-space: nowrap;' + 'padding: 5px;' + 'border: 1px solid white;' + 'z-index: 10000;' + '}' + '.jqsfield { ' + 'color: white;' + 'font: 10px arial, san serif;' + 'text-align: left;' + '}'; /** * Utilities */ createClass = function (/* [baseclass, [mixin, ...]], definition */) { var Class, args; Class = function () { this.init.apply(this, arguments); }; if (arguments.length > 1) { if (arguments[0]) { Class.prototype = $.extend(new arguments[0](), arguments[arguments.length - 1]); Class._super = arguments[0].prototype; } else { Class.prototype = arguments[arguments.length - 1]; } if (arguments.length > 2) { args = Array.prototype.slice.call(arguments, 1, -1); args.unshift(Class.prototype); $.extend.apply($, args); } } else { Class.prototype = arguments[0]; } Class.prototype.cls = Class; return Class; }; /** * Wraps a format string for tooltips * {{x}} * {{x.2} * {{x:months}} */ $.SPFormatClass = SPFormat = createClass({ fre: /\{\{([\w.]+?)(:(.+?))?\}\}/g, precre: /(\w+)\.(\d+)/, init: function (format, fclass) { this.format = format; this.fclass = fclass; }, render: function (fieldset, lookups, options) { var self = this, fields = fieldset, match, token, lookupkey, fieldvalue, prec; return this.format.replace(this.fre, function () { var lookup; token = arguments[1]; lookupkey = arguments[3]; match = self.precre.exec(token); if (match) { prec = match[2]; token = match[1]; } else { prec = false; } fieldvalue = fields[token]; if (fieldvalue === undefined) { return ''; } if (lookupkey && lookups && lookups[lookupkey]) { lookup = lookups[lookupkey]; if (lookup.get) { // RangeMap return lookups[lookupkey].get(fieldvalue) || fieldvalue; } else { return lookups[lookupkey][fieldvalue] || fieldvalue; } } if (isNumber(fieldvalue)) { if (options.get('numberFormatter')) { fieldvalue = options.get('numberFormatter')(fieldvalue); } else { fieldvalue = formatNumber(fieldvalue, prec, options.get('numberDigitGroupCount'), options.get('numberDigitGroupSep'), options.get('numberDecimalMark')); } } return fieldvalue; }); } }); // convience method to avoid needing the new operator $.spformat = function(format, fclass) { return new SPFormat(format, fclass); }; clipval = function (val, min, max) { if (val < min) { return min; } if (val > max) { return max; } return val; }; quartile = function (values, q) { var vl; if (q === 2) { vl = Math.floor(values.length / 2); return values.length % 2 ? values[vl] : (values[vl-1] + values[vl]) / 2; } else { if (values.length % 2 ) { // odd vl = (values.length * q + q) / 4; return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1]; } else { //even vl = (values.length * q + 2) / 4; return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1]; } } }; normalizeValue = function (val) { var nf; switch (val) { case 'undefined': val = undefined; break; case 'null': val = null; break; case 'true': val = true; break; case 'false': val = false; break; default: nf = parseFloat(val); if (val == nf) { val = nf; } } return val; }; normalizeValues = function (vals) { var i, result = []; for (i = vals.length; i--;) { result[i] = normalizeValue(vals[i]); } return result; }; remove = function (vals, filter) { var i, vl, result = []; for (i = 0, vl = vals.length; i < vl; i++) { if (vals[i] !== filter) { result.push(vals[i]); } } return result; }; isNumber = function (num) { return !isNaN(parseFloat(num)) && isFinite(num); }; formatNumber = function (num, prec, groupsize, groupsep, decsep) { var p, i; num = (prec === false ? parseFloat(num).toString() : num.toFixed(prec)).split(''); p = (p = $.inArray('.', num)) < 0 ? num.length : p; if (p < num.length) { num[p] = decsep; } for (i = p - groupsize; i > 0; i -= groupsize) { num.splice(i, 0, groupsep); } return num.join(''); }; // determine if all values of an array match a value // returns true if the array is empty all = function (val, arr, ignoreNull) { var i; for (i = arr.length; i--; ) { if (ignoreNull && arr[i] === null) continue; if (arr[i] !== val) { return false; } } return true; }; // sums the numeric values in an array, ignoring other values sum = function (vals) { var total = 0, i; for (i = vals.length; i--;) { total += typeof vals[i] === 'number' ? vals[i] : 0; } return total; }; ensureArray = function (val) { return $.isArray(val) ? val : [val]; }; // http://paulirish.com/2008/bookmarklet-inject-new-css-rules/ addCSS = function(css) { var tag; //if ('\v' == 'v') /* ie only */ { if (document.createStyleSheet) { document.createStyleSheet().cssText = css; } else { tag = document.createElement('style'); tag.type = 'text/css'; document.getElementsByTagName('head')[0].appendChild(tag); tag[(typeof document.body.style.WebkitAppearance == 'string') /* webkit only */ ? 'innerText' : 'innerHTML'] = css; } }; // Provide a cross-browser interface to a few simple drawing primitives $.fn.simpledraw = function (width, height, useExisting, interact) { var target, mhandler; if (useExisting && (target = this.data('_jqs_vcanvas'))) { return target; } if ($.fn.sparkline.canvas === false) { // We've already determined that neither Canvas nor VML are available return false; } else if ($.fn.sparkline.canvas === undefined) { // No function defined yet -- need to see if we support Canvas or VML var el = document.createElement('canvas'); if (!!(el.getContext && el.getContext('2d'))) { // Canvas is available $.fn.sparkline.canvas = function(width, height, target, interact) { return new VCanvas_canvas(width, height, target, interact); }; } else if (document.namespaces && !document.namespaces.v) { // VML is available document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML'); $.fn.sparkline.canvas = function(width, height, target, interact) { return new VCanvas_vml(width, height, target); }; } else { // Neither Canvas nor VML are available $.fn.sparkline.canvas = false; return false; } } if (width === undefined) { width = $(this).innerWidth(); } if (height === undefined) { height = $(this).innerHeight(); } target = $.fn.sparkline.canvas(width, height, this, interact); mhandler = $(this).data('_jqs_mhandler'); if (mhandler) { mhandler.registerCanvas(target); } return target; }; $.fn.cleardraw = function () { var target = this.data('_jqs_vcanvas'); if (target) { target.reset(); } }; $.RangeMapClass = RangeMap = createClass({ init: function (map) { var key, range, rangelist = []; for (key in map) { if (map.hasOwnProperty(key) && typeof key === 'string' && key.indexOf(':') > -1) { range = key.split(':'); range[0] = range[0].length === 0 ? -Infinity : parseFloat(range[0]); range[1] = range[1].length === 0 ? Infinity : parseFloat(range[1]); range[2] = map[key]; rangelist.push(range); } } this.map = map; this.rangelist = rangelist || false; }, get: function (value) { var rangelist = this.rangelist, i, range, result; if ((result = this.map[value]) !== undefined) { return result; } if (rangelist) { for (i = rangelist.length; i--;) { range = rangelist[i]; if (range[0] <= value && range[1] >= value) { return range[2]; } } } return undefined; } }); // Convenience function $.range_map = function(map) { return new RangeMap(map); }; MouseHandler = createClass({ init: function (el, options) { var $el = $(el); this.$el = $el; this.options = options; this.currentPageX = 0; this.currentPageY = 0; this.el = el; this.splist = []; this.tooltip = null; this.over = false; this.displayTooltips = !options.get('disableTooltips'); this.highlightEnabled = !options.get('disableHighlight'); }, registerSparkline: function (sp) { this.splist.push(sp); if (this.over) { this.updateDisplay(); } }, registerCanvas: function (canvas) { var $canvas = $(canvas.canvas); this.canvas = canvas; this.$canvas = $canvas; $canvas.mouseenter($.proxy(this.mouseenter, this)); $canvas.mouseleave($.proxy(this.mouseleave, this)); $canvas.click($.proxy(this.mouseclick, this)); }, reset: function (removeTooltip) { this.splist = []; if (this.tooltip && removeTooltip) { this.tooltip.remove(); this.tooltip = undefined; } }, mouseclick: function (e) { var clickEvent = $.Event('sparklineClick'); clickEvent.originalEvent = e; clickEvent.sparklines = this.splist; this.$el.trigger(clickEvent); }, mouseenter: function (e) { $(document.body).unbind('mousemove.jqs'); $(document.body).bind('mousemove.jqs', $.proxy(this.mousemove, this)); this.over = true; this.currentPageX = e.pageX; this.currentPageY = e.pageY; this.currentEl = e.target; if (!this.tooltip && this.displayTooltips) { this.tooltip = new Tooltip(this.options); this.tooltip.updatePosition(e.pageX, e.pageY); } this.updateDisplay(); }, mouseleave: function () { $(document.body).unbind('mousemove.jqs'); var splist = this.splist, spcount = splist.length, needsRefresh = false, sp, i; this.over = false; this.currentEl = null; if (this.tooltip) { this.tooltip.remove(); this.tooltip = null; } for (i = 0; i < spcount; i++) { sp = splist[i]; if (sp.clearRegionHighlight()) { needsRefresh = true; } } if (needsRefresh) { this.canvas.render(); } }, mousemove: function (e) { this.currentPageX = e.pageX; this.currentPageY = e.pageY; this.currentEl = e.target; if (this.tooltip) { this.tooltip.updatePosition(e.pageX, e.pageY); } this.updateDisplay(); }, updateDisplay: function () { var splist = this.splist, spcount = splist.length, needsRefresh = false, offset = this.$canvas.offset(), localX = this.currentPageX - offset.left, localY = this.currentPageY - offset.top, tooltiphtml, sp, i, result, changeEvent; if (!this.over) { return; } for (i = 0; i < spcount; i++) { sp = splist[i]; result = sp.setRegionHighlight(this.currentEl, localX, localY); if (result) { needsRefresh = true; } } if (needsRefresh) { changeEvent = $.Event('sparklineRegionChange'); changeEvent.sparklines = this.splist; this.$el.trigger(changeEvent); if (this.tooltip) { tooltiphtml = ''; for (i = 0; i < spcount; i++) { sp = splist[i]; tooltiphtml += sp.getCurrentRegionTooltip(); } this.tooltip.setContent(tooltiphtml); } if (!this.disableHighlight) { this.canvas.render(); } } if (result === null) { this.mouseleave(); } } }); Tooltip = createClass({ sizeStyle: 'position: static !important;' + 'display: block !important;' + 'visibility: hidden !important;' + 'float: left !important;', init: function (options) { var tooltipClassname = options.get('tooltipClassname', 'jqstooltip'), sizetipStyle = this.sizeStyle, offset; this.container = options.get('tooltipContainer') || document.body; this.tooltipOffsetX = options.get('tooltipOffsetX', 10); this.tooltipOffsetY = options.get('tooltipOffsetY', 12); // remove any previous lingering tooltip $('#jqssizetip').remove(); $('#jqstooltip').remove(); this.sizetip = $('<div/>', { id: 'jqssizetip', style: sizetipStyle, 'class': tooltipClassname }); this.tooltip = $('<div/>', { id: 'jqstooltip', 'class': tooltipClassname }).appendTo(this.container); // account for the container's location offset = this.tooltip.offset(); this.offsetLeft = offset.left; this.offsetTop = offset.top; this.hidden = true; $(window).unbind('resize.jqs scroll.jqs'); $(window).bind('resize.jqs scroll.jqs', $.proxy(this.updateWindowDims, this)); this.updateWindowDims(); }, updateWindowDims: function () { this.scrollTop = $(window).scrollTop(); this.scrollLeft = $(window).scrollLeft(); this.scrollRight = this.scrollLeft + $(window).width(); this.updatePosition(); }, getSize: function (content) { this.sizetip.html(content).appendTo(this.container); this.width = this.sizetip.width() + 1; this.height = this.sizetip.height(); this.sizetip.remove(); }, setContent: function (content) { if (!content) { this.tooltip.css('visibility', 'hidden'); this.hidden = true; return; } this.getSize(content); this.tooltip.html(content) .css({ 'width': this.width, 'height': this.height, 'visibility': 'visible' }); if (this.hidden) { this.hidden = false; this.updatePosition(); } }, updatePosition: function (x, y) { if (x === undefined) { if (this.mousex === undefined) { return; } x = this.mousex - this.offsetLeft; y = this.mousey - this.offsetTop; } else { this.mousex = x = x - this.offsetLeft; this.mousey = y = y - this.offsetTop; } if (!this.height || !this.width || this.hidden) { return; } y -= this.height + this.tooltipOffsetY; x += this.tooltipOffsetX; if (y < this.scrollTop) { y = this.scrollTop; } if (x < this.scrollLeft) { x = this.scrollLeft; } else if (x + this.width > this.scrollRight) { x = this.scrollRight - this.width; } this.tooltip.css({ 'left': x, 'top': y }); }, remove: function () { this.tooltip.remove(); this.sizetip.remove(); this.sizetip = this.tooltip = undefined; $(window).unbind('resize.jqs scroll.jqs'); } }); initStyles = function() { addCSS(defaultStyles); }; $(initStyles); pending = []; $.fn.sparkline = function (userValues, userOptions) { return this.each(function () { var options = new $.fn.sparkline.options(this, userOptions), $this = $(this), render, i; render = function () { var values, width, height, tmp, mhandler, sp, vals; if (userValues === 'html' || userValues === undefined) { vals = this.getAttribute(options.get('tagValuesAttribute')); if (vals === undefined || vals === null) { vals = $this.html(); } values = vals.replace(/(^\s*<!--)|(-->\s*$)|\s+/g, '').split(','); } else { values = userValues; } width = options.get('width') === 'auto' ? values.length * options.get('defaultPixelsPerValue') : options.get('width'); if (options.get('height') === 'auto') { if (!options.get('composite') || !$.data(this, '_jqs_vcanvas')) { // must be a better way to get the line height tmp = document.createElement('span'); tmp.innerHTML = 'a'; $this.html(tmp); height = $(tmp).innerHeight() || $(tmp).height(); $(tmp).remove(); tmp = null; } } else { height = options.get('height'); } if (!options.get('disableInteraction')) { mhandler = $.data(this, '_jqs_mhandler'); if (!mhandler) { mhandler = new MouseHandler(this, options); $.data(this, '_jqs_mhandler', mhandler); } else if (!options.get('composite')) { mhandler.reset(); } } else { mhandler = false; } if (options.get('composite') && !$.data(this, '_jqs_vcanvas')) { if (!$.data(this, '_jqs_errnotify')) { alert('Attempted to attach a composite sparkline to an element with no existing sparkline'); $.data(this, '_jqs_errnotify', true); } return; } sp = new $.fn.sparkline[options.get('type')](this, values, options, width, height); sp.render(); if (mhandler) { mhandler.registerSparkline(sp); } }; if (($(this).html() && !options.get('disableHiddenCheck') && $(this).is(':hidden')) || !$(this).parents('body').length) { if (!options.get('composite') && $.data(this, '_jqs_pending')) { // remove any existing references to the element for (i = pending.length; i; i--) { if (pending[i - 1][0] == this) { pending.splice(i - 1, 1); } } } pending.push([this, render]); $.data(this, '_jqs_pending', true); } else { render.call(this); } }); }; $.fn.sparkline.defaults = getDefaults(); $.sparkline_display_visible = function () { var el, i, pl; var done = []; for (i = 0, pl = pending.length; i < pl; i++) { el = pending[i][0]; if ($(el).is(':visible') && !$(el).parents().is(':hidden')) { pending[i][1].call(el); $.data(pending[i][0], '_jqs_pending', false); done.push(i); } else if (!$(el).closest('html').length && !$.data(el, '_jqs_pending')) { // element has been inserted and removed from the DOM // If it was not yet inserted into the dom then the .data request // will return true. // removing from the dom causes the data to be removed. $.data(pending[i][0], '_jqs_pending', false); done.push(i); } } for (i = done.length; i; i--) { pending.splice(done[i - 1], 1); } }; /** * User option handler */ $.fn.sparkline.options = createClass({ init: function (tag, userOptions) { var extendedOptions, defaults, base, tagOptionType; this.userOptions = userOptions = userOptions || {}; this.tag = tag; this.tagValCache = {}; defaults = $.fn.sparkline.defaults; base = defaults.common; this.tagOptionsPrefix = userOptions.enableTagOptions && (userOptions.tagOptionsPrefix || base.tagOptionsPrefix); tagOptionType = this.getTagSetting('type'); if (tagOptionType === UNSET_OPTION) { extendedOptions = defaults[userOptions.type || base.type]; } else { extendedOptions = defaults[tagOptionType]; } this.mergedOptions = $.extend({}, base, extendedOptions, userOptions); }, getTagSetting: function (key) { var prefix = this.tagOptionsPrefix, val, i, pairs, keyval; if (prefix === false || prefix === undefined) { return UNSET_OPTION; } if (this.tagValCache.hasOwnProperty(key)) { val = this.tagValCache.key; } else { val = this.tag.getAttribute(prefix + key); if (val === undefined || val === null) { val = UNSET_OPTION; } else if (val.substr(0, 1) === '[') { val = val.substr(1, val.length - 2).split(','); for (i = val.length; i--;) { val[i] = normalizeValue(val[i].replace(/(^\s*)|(\s*$)/g, '')); } } else if (val.substr(0, 1) === '{') { pairs = val.substr(1, val.length - 2).split(','); val = {}; for (i = pairs.length; i--;) { keyval = pairs[i].split(':', 2); val[keyval[0].replace(/(^\s*)|(\s*$)/g, '')] = normalizeValue(keyval[1].replace(/(^\s*)|(\s*$)/g, '')); } } else { val = normalizeValue(val); } this.tagValCache.key = val; } return val; }, get: function (key, defaultval) { var tagOption = this.getTagSetting(key), result; if (tagOption !== UNSET_OPTION) { return tagOption; } return (result = this.mergedOptions[key]) === undefined ? defaultval : result; } }); $.fn.sparkline._base = createClass({ disabled: false, init: function (el, values, options, width, height) { this.el = el; this.$el = $(el); this.values = values; this.options = options; this.width = width; this.height = height; this.currentRegion = undefined; }, /** * Setup the canvas */ initTarget: function () { var interactive = !this.options.get('disableInteraction'); if (!(this.target = this.$el.simpledraw(this.width, this.height, this.options.get('composite'), interactive))) { this.disabled = true; } else { this.canvasWidth = this.target.pixelWidth; this.canvasHeight = this.target.pixelHeight; } }, /** * Actually render the chart to the canvas */ render: function () { if (this.disabled) { this.el.innerHTML = ''; return false; } return true; }, /** * Return a region id for a given x/y co-ordinate */ getRegion: function (x, y) { }, /** * Highlight an item based on the moused-over x,y co-ordinate */ setRegionHighlight: function (el, x, y) { var currentRegion = this.currentRegion, highlightEnabled = !this.options.get('disableHighlight'), newRegion; if (x > this.canvasWidth || y > this.canvasHeight || x < 0 || y < 0) { return null; } newRegion = this.getRegion(el, x, y); if (currentRegion !== newRegion) { if (currentRegion !== undefined && highlightEnabled) { this.removeHighlight(); } this.currentRegion = newRegion; if (newRegion !== undefined && highlightEnabled) { this.renderHighlight(); } return true; } return false; }, /** * Reset any currently highlighted item */ clearRegionHighlight: function () { if (this.currentRegion !== undefined) { this.removeHighlight(); this.currentRegion = undefined; return true; } return false; }, renderHighlight: function () { this.changeHighlight(true); }, removeHighlight: function () { this.changeHighlight(false); }, changeHighlight: function (highlight) {}, /** * Fetch the HTML to display as a tooltip */ getCurrentRegionTooltip: function () { var options = this.options, header = '', entries = [], fields, formats, formatlen, fclass, text, i, showFields, showFieldsKey, newFields, fv, formatter, format, fieldlen, j; if (this.currentRegion === undefined) { return ''; } fields = this.getCurrentRegionFields(); formatter = options.get('tooltipFormatter'); if (formatter) { return formatter(this, options, fields); } if (options.get('tooltipChartTitle')) { header += '<div class="jqs jqstitle">' + options.get('tooltipChartTitle') + '</div>\n'; } formats = this.options.get('tooltipFormat'); if (!formats) { return ''; } if (!$.isArray(formats)) { formats = [formats]; } if (!$.isArray(fields)) { fields = [fields]; } showFields = this.options.get('tooltipFormatFieldlist'); showFieldsKey = this.options.get('tooltipFormatFieldlistKey'); if (showFields && showFieldsKey) { // user-selected ordering of fields newFields = []; for (i = fields.length; i--;) { fv = fields[i][showFieldsKey]; if ((j = $.inArray(fv, showFields)) != -1) { newFields[j] = fields[i]; } } fields = newFields; } formatlen = formats.length; fieldlen = fields.length; for (i = 0; i < formatlen; i++) { format = formats[i]; if (typeof format === 'string') { format = new SPFormat(format); } fclass = format.fclass || 'jqsfield'; for (j = 0; j < fieldlen; j++) { if (!fields[j].isNull || !options.get('tooltipSkipNull')) { $.extend(fields[j], { prefix: options.get('tooltipPrefix'), suffix: options.get('tooltipSuffix') }); text = format.render(fields[j], options.get('tooltipValueLookups'), options); entries.push('<div class="' + fclass + '">' + text + '</div>'); } } } if (entries.length) { return header + entries.join('\n'); } return ''; }, getCurrentRegionFields: function () {}, calcHighlightColor: function (color, options) { var highlightColor = options.get('highlightColor'), lighten = options.get('highlightLighten'), parse, mult, rgbnew, i; if (highlightColor) { return highlightColor; } if (lighten) { // extract RGB values parse = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(color) || /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color); if (parse) { rgbnew = []; mult = color.length === 4 ? 16 : 1; for (i = 0; i < 3; i++) { rgbnew[i] = clipval(Math.round(parseInt(parse[i + 1], 16) * mult * lighten), 0, 255); } return 'rgb(' + rgbnew.join(',') + ')'; } } return color; } }); barHighlightMixin = { changeHighlight: function (highlight) { var currentRegion = this.currentRegion, target = this.target, shapeids = this.regionShapes[currentRegion], newShapes; // will be null if the region value was null if (shapeids) { newShapes = this.renderRegion(currentRegion, highlight); if ($.isArray(newShapes) || $.isArray(shapeids)) { target.replaceWithShapes(shapeids, newShapes); this.regionShapes[currentRegion] = $.map(newShapes, function (newShape) { return newShape.id; }); } else { target.replaceWithShape(shapeids, newShapes); this.regionShapes[currentRegion] = newShapes.id; } } }, render: function () { var values = this.values, target = this.target, regionShapes = this.regionShapes, shapes, ids, i, j; if (!this.cls._super.render.call(this)) { return; } for (i = values.length; i--;) { shapes = this.renderRegion(i); if (shapes) { if ($.isArray(shapes)) { ids = []; for (j = shapes.length; j--;) { shapes[j].append(); ids.push(shapes[j].id); } regionShapes[i] = ids; } else { shapes.append(); regionShapes[i] = shapes.id; // store just the shapeid } } else { // null value regionShapes[i] = null; } } target.render(); } }; /** * Line charts */ $.fn.sparkline.line = line = createClass($.fn.sparkline._base, { type: 'line', init: function (el, values, options, width, height) { line._super.init.call(this, el, values, options, width, height); this.vertices = []; this.regionMap = []; this.xvalues = []; this.yvalues = []; this.yminmax = []; this.hightlightSpotId = null; this.lastShapeId = null; this.initTarget(); }, getRegion: function (el, x, y) { var i, regionMap = this.regionMap; // maps regions to value positions for (i = regionMap.length; i--;) { if (regionMap[i] !== null && x >= regionMap[i][0] && x <= regionMap[i][1]) { return regionMap[i][2]; } } return undefined; }, getCurrentRegionFields: function () { var currentRegion = this.currentRegion; return { isNull: this.yvalues[currentRegion] === null, x: this.xvalues[currentRegion], y: this.yvalues[currentRegion], color: this.options.get('lineColor'), fillColor: this.options.get('fillColor'), offset: currentRegion }; }, renderHighlight: function () { var currentRegion = this.currentRegion, target = this.target, vertex = this.vertices[currentRegion], options = this.options, spotRadius = options.get('spotRadius'), highlightSpotColor = options.get('highlightSpotColor'), highlightLineColor = options.get('highlightLineColor'), highlightSpot, highlightLine; if (!vertex) { return; } if (spotRadius && highlightSpotColor) { highlightSpot = target.drawCircle(vertex[0], vertex[1], spotRadius, undefined, highlightSpotColor); this.highlightSpotId = highlightSpot.id; target.insertAfterShape(this.lastShapeId, highlightSpot); } if (highlightLineColor) { highlightLine = target.drawLine(vertex[0], this.canvasTop, vertex[0], this.canvasTop + this.canvasHeight, highlightLineColor); this.highlightLineId = highlightLine.id; target.insertAfterShape(this.lastShapeId, highlightLine); } }, removeHighlight: function () { var target = this.target; if (this.highlightSpotId) { target.removeShapeId(this.highlightSpotId); this.highlightSpotId = null; } if (this.highlightLineId) { target.removeShapeId(this.highlightLineId); this.highlightLineId = null; } }, scanValues: function () { var values = this.values, valcount = values.length, xvalues = this.xvalues, yvalues = this.yvalues, yminmax = this.yminmax, i, val, isStr, isArray, sp; for (i = 0; i < valcount; i++) { val = values[i]; isStr = typeof(values[i]) === 'string'; isArray = typeof(values[i]) === 'object' && values[i] instanceof Array; sp = isStr && values[i].split(':'); if (isStr && sp.length === 2) { // x:y xvalues.push(Number(sp[0])); yvalues.push(Number(sp[1])); yminmax.push(Number(sp[1])); } else if (isArray) { xvalues.push(val[0]); yvalues.push(val[1]); yminmax.push(val[1]); } else { xvalues.push(i); if (values[i] === null || values[i] === 'null') { yvalues.push(null); } else { yvalues.push(Number(val)); yminmax.push(Number(val)); } } } if (this.options.get('xvalues')) { xvalues = this.options.get('xvalues'); } this.maxy = this.maxyorg = Math.max.apply(Math, yminmax); this.miny = this.minyorg = Math.min.apply(Math, yminmax); this.maxx = Math.max.apply(Math, xvalues); this.minx = Math.min.apply(Math, xvalues); this.xvalues = xvalues; this.yvalues = yvalues; this.yminmax = yminmax; }, processRangeOptions: function () { var options = this.options, normalRangeMin = options.get('normalRangeMin'), normalRangeMax = options.get('normalRangeMax'); if (normalRangeMin !== undefined) { if (normalRangeMin < this.miny) { this.miny = normalRangeMin; } if (normalRangeMax > this.maxy) { this.maxy = normalRangeMax; } } if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.miny)) { this.miny = options.get('chartRangeMin'); } if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.maxy)) { this.maxy = options.get('chartRangeMax'); } if (options.get('chartRangeMinX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMinX') < this.minx)) { this.minx = options.get('chartRangeMinX'); } if (options.get('chartRangeMaxX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMaxX') > this.maxx)) { this.maxx = options.get('chartRangeMaxX'); } }, drawNormalRange: function (canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey) { var normalRangeMin = this.options.get('normalRangeMin'), normalRangeMax = this.options.get('normalRangeMax'), ytop = canvasTop + Math.round(canvasHeight - (canvasHeight * ((normalRangeMax - this.miny) / rangey))), height = Math.round((canvasHeight * (normalRangeMax - normalRangeMin)) / rangey); this.target.drawRect(canvasLeft, ytop, canvasWidth, height, undefined, this.options.get('normalRangeColor')).append(); }, render: function () { var options = this.options, target = this.target, canvasWidth = this.canvasWidth, canvasHeight = this.canvasHeight, vertices = this.vertices, spotRadius = options.get('spotRadius'), regionMap = this.regionMap, rangex, rangey, yvallast, canvasTop, canvasLeft, vertex, path, paths, x, y, xnext, xpos, xposnext, last, next, yvalcount, lineShapes, fillShapes, plen, valueSpots, hlSpotsEnabled, color, xvalues, yvalues, i; if (!line._super.render.call(this)) { return; } this.scanValues(); this.processRangeOptions(); xvalues = this.xvalues; yvalues = this.yvalues; if (!this.yminmax.length || this.yvalues.length < 2) { // empty or all null valuess return; } canvasTop = canvasLeft = 0; rangex = this.maxx - this.minx === 0 ? 1 : this.maxx - this.minx; rangey = this.maxy - this.miny === 0 ? 1 : this.maxy - this.miny; yvallast = this.yvalues.length - 1; if (spotRadius && (canvasWidth < (spotRadius * 4) || canvasHeight < (spotRadius * 4))) { spotRadius = 0; } if (spotRadius) { // adjust the canvas size as required so that spots will fit hlSpotsEnabled = options.get('highlightSpotColor') && !options.get('disableInteraction'); if (hlSpotsEnabled || options.get('minSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.miny)) { canvasHeight -= Math.ceil(spotRadius); } if (hlSpotsEnabled || options.get('maxSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.maxy)) { canvasHeight -= Math.ceil(spotRadius); canvasTop += Math.ceil(spotRadius); } if (hlSpotsEnabled || ((options.get('minSpotColor') || options.get('maxSpotColor')) && (yvalues[0] === this.miny || yvalues[0] === this.maxy))) { canvasLeft += Math.ceil(spotRadius); canvasWidth -= Math.ceil(spotRadius); } if (hlSpotsEnabled || options.get('spotColor') || (options.get('minSpotColor') || options.get('maxSpotColor') && (yvalues[yvallast] === this.miny || yvalues[yvallast] === this.maxy))) { canvasWidth -= Math.ceil(spotRadius); } } canvasHeight--; if (options.get('normalRangeMin') !== undefined && !options.get('drawNormalOnTop')) { this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey); } path = []; paths = [path]; last = next = null; yvalcount = yvalues.length; for (i = 0; i < yvalcount; i++) { x = xvalues[i]; xnext = xvalues[i + 1]; y = yvalues[i]; xpos = canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)); xposnext = i < yvalcount - 1 ? canvasLeft + Math.round((xnext - this.minx) * (canvasWidth / rangex)) : canvasWidth; next = xpos + ((xposnext - xpos) / 2); regionMap[i] = [last || 0, next, i]; last = next; if (y === null) { if (i) { if (yvalues[i - 1] !== null) { path = []; paths.push(path); } vertices.push(null); } } else { if (y < this.miny) { y = this.miny; } if (y > this.maxy) { y = this.maxy; } if (!path.length) { // previous value was null path.push([xpos, canvasTop + canvasHeight]); } vertex = [xpos, canvasTop + Math.round(canvasHeight - (canvasHeight * ((y - this.miny) / rangey)))]; path.push(vertex); vertices.push(vertex); } } lineShapes = []; fillShapes = []; plen = paths.length; for (i = 0; i < plen; i++) { path = paths[i]; if (path.length) { if (options.get('fillColor')) { path.push([path[path.length - 1][0], (canvasTop + canvasHeight)]); fillShapes.push(path.slice(0)); path.pop(); } // if there's only a single point in this path, then we want to display it // as a vertical line which means we keep path[0] as is if (path.length > 2) { // else we want the first value path[0] = [path[0][0], path[1][1]]; } lineShapes.push(path); } } // draw the fill first, then optionally the normal range, then the line on top of that plen = fillShapes.length; for (i = 0; i < plen; i++) { target.drawShape(fillShapes[i], options.get('fillColor'), options.get('fillColor')).append(); } if (options.get('normalRangeMin') !== undefined && options.get('drawNormalOnTop')) { this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey); } plen = lineShapes.length; for (i = 0; i < plen; i++) { target.drawShape(lineShapes[i], options.get('lineColor'), undefined, options.get('lineWidth')).append(); } if (spotRadius && options.get('valueSpots')) { valueSpots = options.get('valueSpots'); if (valueSpots.get === undefined) { valueSpots = new RangeMap(valueSpots); } for (i = 0; i < yvalcount; i++) { color = valueSpots.get(yvalues[i]); if (color) { target.drawCircle(canvasLeft + Math.round((xvalues[i] - this.minx) * (canvasWidth / rangex)), canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[i] - this.miny) / rangey))), spotRadius, undefined, color).append(); } } } if (spotRadius && options.get('spotColor') && yvalues[yvallast] !== null) { target.drawCircle(canvasLeft + Math.round((xvalues[xvalues.length - 1] - this.minx) * (canvasWidth / rangex)), canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[yvallast] - this.miny) / rangey))), spotRadius, undefined, options.get('spotColor')).append(); } if (this.maxy !== this.minyorg) { if (spotRadius && options.get('minSpotColor')) { x = xvalues[$.inArray(this.minyorg, yvalues)]; target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)), canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.minyorg - this.miny) / rangey))), spotRadius, undefined, options.get('minSpotColor')).append(); } if (spotRadius && options.get('maxSpotColor')) { x = xvalues[$.inArray(this.maxyorg, yvalues)]; target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)), canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.maxyorg - this.miny) / rangey))), spotRadius, undefined, options.get('maxSpotColor')).append(); } } this.lastShapeId = target.getLastShapeId(); this.canvasTop = canvasTop; target.render(); } }); /** * Bar charts */ $.fn.sparkline.bar = bar = createClass($.fn.sparkline._base, barHighlightMixin, { type: 'bar', init: function (el, values, options, width, height) { var barWidth = parseInt(options.get('barWidth'), 10), barSpacing = parseInt(options.get('barSpacing'), 10), chartRangeMin = options.get('chartRangeMin'), chartRangeMax = options.get('chartRangeMax'), chartRangeClip = options.get('chartRangeClip'), stackMin = Infinity, stackMax = -Infinity, isStackString, groupMin, groupMax, stackRanges, numValues, i, vlen, range, zeroAxis, xaxisOffset, min, max, clipMin, clipMax, stacked, vlist, j, slen, svals, val, yoffset, yMaxCalc, canvasHeightEf; bar._super.init.call(this, el, values, options, width, height); // scan values to determine whether to stack bars for (i = 0, vlen = values.length; i < vlen; i++) { val = values[i]; isStackString = typeof(val) === 'string' && val.indexOf(':') > -1; if (isStackString || $.isArray(val)) { stacked = true; if (isStackString) { val = values[i] = normalizeValues(val.split(':')); } val = remove(val, null); // min/max will treat null as zero groupMin = Math.min.apply(Math, val); groupMax = Math.max.apply(Math, val); if (groupMin < stackMin) { stackMin = groupMin; } if (groupMax > stackMax) { stackMax = groupMax; } } } this.stacked = stacked; this.regionShapes = {}; this.barWidth = barWidth; this.barSpacing = barSpacing; this.totalBarWidth = barWidth + barSpacing; this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing); this.initTarget(); if (chartRangeClip) { clipMin = chartRangeMin === undefined ? -Infinity : chartRangeMin; clipMax = chartRangeMax === undefined ? Infinity : chartRangeMax; } numValues = []; stackRanges = stacked ? [] : numValues; var stackTotals = []; var stackRangesNeg = []; for (i = 0, vlen = values.length; i < vlen; i++) { if (stacked) { vlist = values[i]; values[i] = svals = []; stackTotals[i] = 0; stackRanges[i] = stackRangesNeg[i] = 0; for (j = 0, slen = vlist.length; j < slen; j++) { val = svals[j] = chartRangeClip ? clipval(vlist[j], clipMin, clipMax) : vlist[j]; if (val !== null) { if (val > 0) { stackTotals[i] += val; } if (stackMin < 0 && stackMax > 0) { if (val < 0) { stackRangesNeg[i] += Math.abs(val); } else { stackRanges[i] += val; } } else { stackRanges[i] += Math.abs(val - (val < 0 ? stackMax : stackMin)); } numValues.push(val); } } } else { val = chartRangeClip ? clipval(values[i], clipMin, clipMax) : values[i]; val = values[i] = normalizeValue(val); if (val !== null) { numValues.push(val); } } } this.max = max = Math.max.apply(Math, numValues); this.min = min = Math.min.apply(Math, numValues); this.stackMax = stackMax = stacked ? Math.max.apply(Math, stackTotals) : max; this.stackMin = stackMin = stacked ? Math.min.apply(Math, numValues) : min; if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < min)) { min = options.get('chartRangeMin'); } if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > max)) { max = options.get('chartRangeMax'); } this.zeroAxis = zeroAxis = options.get('zeroAxis', true); if (min <= 0 && max >= 0 && zeroAxis) { xaxisOffset = 0; } else if (zeroAxis == false) { xaxisOffset = min; } else if (min > 0) { xaxisOffset = min; } else { xaxisOffset = max; } this.xaxisOffset = xaxisOffset; range = stacked ? (Math.max.apply(Math, stackRanges) + Math.max.apply(Math, stackRangesNeg)) : max - min; // as we plot zero/min values a single pixel line, we add a pixel to all other // values - Reduce the effective canvas size to suit this.canvasHeightEf = (zeroAxis && min < 0) ? this.canvasHeight - 2 : this.canvasHeight - 1; if (min < xaxisOffset) { yMaxCalc = (stacked && max >= 0) ? stackMax : max; yoffset = (yMaxCalc - xaxisOffset) / range * this.canvasHeight; if (yoffset !== Math.ceil(yoffset)) { this.canvasHeightEf -= 2; yoffset = Math.ceil(yoffset); } } else { yoffset = this.canvasHeight; } this.yoffset = yoffset; if ($.isArray(options.get('colorMap'))) { this.colorMapByIndex = options.get('colorMap'); this.colorMapByValue = null; } else { this.colorMapByIndex = null; this.colorMapByValue = options.get('colorMap'); if (this.colorMapByValue && this.colorMapByValue.get === undefined) { this.colorMapByValue = new RangeMap(this.colorMapByValue); } } this.range = range; }, getRegion: function (el, x, y) { var result = Math.floor(x / this.totalBarWidth); return (result < 0 || result >= this.values.length) ? undefined : result; }, getCurrentRegionFields: function () { var currentRegion = this.currentRegion, values = ensureArray(this.values[currentRegion]), result = [], value, i; for (i = values.length; i--;) { value = values[i]; result.push({ isNull: value === null, value: value, color: this.calcColor(i, value, currentRegion), offset: currentRegion }); } return result; }, calcColor: function (stacknum, value, valuenum) { var colorMapByIndex = this.colorMapByIndex, colorMapByValue = this.colorMapByValue, options = this.options, color, newColor; if (this.stacked) { color = options.get('stackedBarColor'); } else { color = (value < 0) ? options.get('negBarColor') : options.get('barColor'); } if (value === 0 && options.get('zeroColor') !== undefined) { color = options.get('zeroColor'); } if (colorMapByValue && (newColor = colorMapByValue.get(value))) { color = newColor; } else if (colorMapByIndex && colorMapByIndex.length > valuenum) { color = colorMapByIndex[valuenum]; } return $.isArray(color) ? color[stacknum % color.length] : color; }, /** * Render bar(s) for a region */ renderRegion: function (valuenum, highlight) { var vals = this.values[valuenum], options = this.options, xaxisOffset = this.xaxisOffset, result = [], range = this.range, stacked = this.stacked, target = this.target, x = valuenum * this.totalBarWidth, canvasHeightEf = this.canvasHeightEf, yoffset = this.yoffset, y, height, color, isNull, yoffsetNeg, i, valcount, val, minPlotted, allMin; vals = $.isArray(vals) ? vals : [vals]; valcount = vals.length; val = vals[0]; isNull = all(null, vals); allMin = all(xaxisOffset, vals, true); if (isNull) { if (options.get('nullColor')) { color = highlight ? options.get('nullColor') : this.calcHighlightColor(options.get('nullColor'), options); y = (yoffset > 0) ? yoffset - 1 : yoffset; return target.drawRect(x, y, this.barWidth - 1, 0, color, color); } else { return undefined; } } yoffsetNeg = yoffset; for (i = 0; i < valcount; i++) { val = vals[i]; if (stacked && val === xaxisOffset) { if (!allMin || minPlotted) { continue; } minPlotted = true; } if (range > 0) { height = Math.floor(canvasHeightEf * ((Math.abs(val - xaxisOffset) / range))) + 1; } else { height = 1; } if (val < xaxisOffset || (val === xaxisOffset && yoffset === 0)) { y = yoffsetNeg; yoffsetNeg += height; } else { y = yoffset - height; yoffset -= height; } color = this.calcColor(i, val, valuenum); if (highlight) { color = this.calcHighlightColor(color, options); } result.push(target.drawRect(x, y, this.barWidth - 1, height - 1, color, color)); } if (result.length === 1) { return result[0]; } return result; } }); /** * Tristate charts */ $.fn.sparkline.tristate = tristate = createClass($.fn.sparkline._base, barHighlightMixin, { type: 'tristate', init: function (el, values, options, width, height) { var barWidth = parseInt(options.get('barWidth'), 10), barSpacing = parseInt(options.get('barSpacing'), 10); tristate._super.init.call(this, el, values, options, width, height); this.regionShapes = {}; this.barWidth = barWidth; this.barSpacing = barSpacing; this.totalBarWidth = barWidth + barSpacing; this.values = $.map(values, Number); this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing); if ($.isArray(options.get('colorMap'))) { this.colorMapByIndex = options.get('colorMap'); this.colorMapByValue = null; } else { this.colorMapByIndex = null; this.colorMapByValue = options.get('colorMap'); if (this.colorMapByValue && this.colorMapByValue.get === undefined) { this.colorMapByValue = new RangeMap(this.colorMapByValue); } } this.initTarget(); }, getRegion: function (el, x, y) { return Math.floor(x / this.totalBarWidth); }, getCurrentRegionFields: function () { var currentRegion = this.currentRegion; return { isNull: this.values[currentRegion] === undefined, value: this.values[currentRegion], color: this.calcColor(this.values[currentRegion], currentRegion), offset: currentRegion }; }, calcColor: function (value, valuenum) { var values = this.values, options = this.options, colorMapByIndex = this.colorMapByIndex, colorMapByValue = this.colorMapByValue, color, newColor; if (colorMapByValue && (newColor = colorMapByValue.get(value))) { color = newColor; } else if (colorMapByIndex && colorMapByIndex.length > valuenum) { color = colorMapByIndex[valuenum]; } else if (values[valuenum] < 0) { color = options.get('negBarColor'); } else if (values[valuenum] > 0) { color = options.get('posBarColor'); } else { color = options.get('zeroBarColor'); } return color; }, renderRegion: function (valuenum, highlight) { var values = this.values, options = this.options, target = this.target, canvasHeight, height, halfHeight, x, y, color; canvasHeight = target.pixelHeight; halfHeight = Math.round(canvasHeight / 2); x = valuenum * this.totalBarWidth; if (values[valuenum] < 0) { y = halfHeight; height = halfHeight - 1; } else if (values[valuenum] > 0) { y = 0; height = halfHeight - 1; } else { y = halfHeight - 1; height = 2; } color = this.calcColor(values[valuenum], valuenum); if (color === null) { return; } if (highlight) { color = this.calcHighlightColor(color, options); } return target.drawRect(x, y, this.barWidth - 1, height - 1, color, color); } }); /** * Discrete charts */ $.fn.sparkline.discrete = discrete = createClass($.fn.sparkline._base, barHighlightMixin, { type: 'discrete', init: function (el, values, options, width, height) { discrete._super.init.call(this, el, values, options, width, height); this.regionShapes = {}; this.values = values = $.map(values, Number); this.min = Math.min.apply(Math, values); this.max = Math.max.apply(Math, values); this.range = this.max - this.min; this.width = width = options.get('width') === 'auto' ? values.length * 2 : this.width; this.interval = Math.floor(width / values.length); this.itemWidth = width / values.length; if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.min)) { this.min = options.get('chartRangeMin'); } if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.max)) { this.max = options.get('chartRangeMax'); } this.initTarget(); if (this.target) { this.lineHeight = options.get('lineHeight') === 'auto' ? Math.round(this.canvasHeight * 0.3) : options.get('lineHeight'); } }, getRegion: function (el, x, y) { return Math.floor(x / this.itemWidth); }, getCurrentRegionFields: function () { var currentRegion = this.currentRegion; return { isNull: this.values[currentRegion] === undefined, value: this.values[currentRegion], offset: currentRegion }; }, renderRegion: function (valuenum, highlight) { var values = this.values, options = this.options, min = this.min, max = this.max, range = this.range, interval = this.interval, target = this.target, canvasHeight = this.canvasHeight, lineHeight = this.lineHeight, pheight = canvasHeight - lineHeight, ytop, val, color, x; val = clipval(values[valuenum], min, max); x = valuenum * interval; ytop = Math.round(pheight - pheight * ((val - min) / range)); color = (options.get('thresholdColor') && val < options.get('thresholdValue')) ? options.get('thresholdColor') : options.get('lineColor'); if (highlight) { color = this.calcHighlightColor(color, options); } return target.drawLine(x, ytop, x, ytop + lineHeight, color); } }); /** * Bullet charts */ $.fn.sparkline.bullet = bullet = createClass($.fn.sparkline._base, { type: 'bullet', init: function (el, values, options, width, height) { var min, max, vals; bullet._super.init.call(this, el, values, options, width, height); // values: target, performance, range1, range2, range3 this.values = values = normalizeValues(values); // target or performance could be null vals = values.slice(); vals[0] = vals[0] === null ? vals[2] : vals[0]; vals[1] = values[1] === null ? vals[2] : vals[1]; min = Math.min.apply(Math, values); max = Math.max.apply(Math, values); if (options.get('base') === undefined) { min = min < 0 ? min : 0; } else { min = options.get('base'); } this.min = min; this.max = max; this.range = max - min; this.shapes = {}; this.valueShapes = {}; this.regiondata = {}; this.width = width = options.get('width') === 'auto' ? '4.0em' : width; this.target = this.$el.simpledraw(width, height, options.get('composite')); if (!values.length) { this.disabled = true; } this.initTarget(); }, getRegion: function (el, x, y) { var shapeid = this.target.getShapeAt(el, x, y); return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined; }, getCurrentRegionFields: function () { var currentRegion = this.currentRegion; return { fieldkey: currentRegion.substr(0, 1), value: this.values[currentRegion.substr(1)], region: currentRegion }; }, changeHighlight: function (highlight) { var currentRegion = this.currentRegion, shapeid = this.valueShapes[currentRegion], shape; delete this.shapes[shapeid]; switch (currentRegion.substr(0, 1)) { case 'r': shape = this.renderRange(currentRegion.substr(1), highlight); break; case 'p': shape = this.renderPerformance(highlight); break; case 't': shape = this.renderTarget(highlight); break; } this.valueShapes[currentRegion] = shape.id; this.shapes[shape.id] = currentRegion; this.target.replaceWithShape(shapeid, shape); }, renderRange: function (rn, highlight) { var rangeval = this.values[rn], rangewidth = Math.round(this.canvasWidth * ((rangeval - this.min) / this.range)), color = this.options.get('rangeColors')[rn - 2]; if (highlight) { color = this.calcHighlightColor(color, this.options); } return this.target.drawRect(0, 0, rangewidth - 1, this.canvasHeight - 1, color, color); }, renderPerformance: function (highlight) { var perfval = this.values[1], perfwidth = Math.round(this.canvasWidth * ((perfval - this.min) / this.range)), color = this.options.get('performanceColor'); if (highlight) { color = this.calcHighlightColor(color, this.options); } return this.target.drawRect(0, Math.round(this.canvasHeight * 0.3), perfwidth - 1, Math.round(this.canvasHeight * 0.4) - 1, color, color); }, renderTarget: function (highlight) { var targetval = this.values[0], x = Math.round(this.canvasWidth * ((targetval - this.min) / this.range) - (this.options.get('targetWidth') / 2)), targettop = Math.round(this.canvasHeight * 0.10), targetheight = this.canvasHeight - (targettop * 2), color = this.options.get('targetColor'); if (highlight) { color = this.calcHighlightColor(color, this.options); } return this.target.drawRect(x, targettop, this.options.get('targetWidth') - 1, targetheight - 1, color, color); }, render: function () { var vlen = this.values.length, target = this.target, i, shape; if (!bullet._super.render.call(this)) { return; } for (i = 2; i < vlen; i++) { shape = this.renderRange(i).append(); this.shapes[shape.id] = 'r' + i; this.valueShapes['r' + i] = shape.id; } if (this.values[1] !== null) { shape = this.renderPerformance().append(); this.shapes[shape.id] = 'p1'; this.valueShapes.p1 = shape.id; } if (this.values[0] !== null) { shape = this.renderTarget().append(); this.shapes[shape.id] = 't0'; this.valueShapes.t0 = shape.id; } target.render(); } }); /** * Pie charts */ $.fn.sparkline.pie = pie = createClass($.fn.sparkline._base, { type: 'pie', init: function (el, values, options, width, height) { var total = 0, i; pie._super.init.call(this, el, values, options, width, height); this.shapes = {}; // map shape ids to value offsets this.valueShapes = {}; // maps value offsets to shape ids this.values = values = $.map(values, Number); if (options.get('width') === 'auto') { this.width = this.height; } if (values.length > 0) { for (i = values.length; i--;) { total += values[i]; } } this.total = total; this.initTarget(); this.radius = Math.floor(Math.min(this.canvasWidth, this.canvasHeight) / 2); }, getRegion: function (el, x, y) { var shapeid = this.target.getShapeAt(el, x, y); return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined; }, getCurrentRegionFields: function () { var currentRegion = this.currentRegion; return { isNull: this.values[currentRegion] === undefined, value: this.values[currentRegion], percent: this.values[currentRegion] / this.total * 100, color: this.options.get('sliceColors')[currentRegion % this.options.get('sliceColors').length], offset: currentRegion }; }, changeHighlight: function (highlight) { var currentRegion = this.currentRegion, newslice = this.renderSlice(currentRegion, highlight), shapeid = this.valueShapes[currentRegion]; delete this.shapes[shapeid]; this.target.replaceWithShape(shapeid, newslice); this.valueShapes[currentRegion] = newslice.id; this.shapes[newslice.id] = currentRegion; }, renderSlice: function (valuenum, highlight) { var target = this.target, options = this.options, radius = this.radius, borderWidth = options.get('borderWidth'), offset = options.get('offset'), circle = 2 * Math.PI, values = this.values, total = this.total, next = offset ? (2*Math.PI)*(offset/360) : 0, start, end, i, vlen, color; vlen = values.length; for (i = 0; i < vlen; i++) { start = next; end = next; if (total > 0) { // avoid divide by zero end = next + (circle * (values[i] / total)); } if (valuenum === i) { color = options.get('sliceColors')[i % options.get('sliceColors').length]; if (highlight) { color = this.calcHighlightColor(color, options); } return target.drawPieSlice(radius, radius, radius - borderWidth, start, end, undefined, color); } next = end; } }, render: function () { var target = this.target, values = this.values, options = this.options, radius = this.radius, borderWidth = options.get('borderWidth'), shape, i; if (!pie._super.render.call(this)) { return; } if (borderWidth) { target.drawCircle(radius, radius, Math.floor(radius - (borderWidth / 2)), options.get('borderColor'), undefined, borderWidth).append(); } for (i = values.length; i--;) { if (values[i]) { // don't render zero values shape = this.renderSlice(i).append(); this.valueShapes[i] = shape.id; // store just the shapeid this.shapes[shape.id] = i; } } target.render(); } }); /** * Box plots */ $.fn.sparkline.box = box = createClass($.fn.sparkline._base, { type: 'box', init: function (el, values, options, width, height) { box._super.init.call(this, el, values, options, width, height); this.values = $.map(values, Number); this.width = options.get('width') === 'auto' ? '4.0em' : width; this.initTarget(); if (!this.values.length) { this.disabled = 1; } }, /** * Simulate a single region */ getRegion: function () { return 1; }, getCurrentRegionFields: function () { var result = [ { field: 'lq', value: this.quartiles[0] }, { field: 'med', value: this.quartiles[1] }, { field: 'uq', value: this.quartiles[2] } ]; if (this.loutlier !== undefined) { result.push({ field: 'lo', value: this.loutlier}); } if (this.routlier !== undefined) { result.push({ field: 'ro', value: this.routlier}); } if (this.lwhisker !== undefined) { result.push({ field: 'lw', value: this.lwhisker}); } if (this.rwhisker !== undefined) { result.push({ field: 'rw', value: this.rwhisker}); } return result; }, render: function () { var target = this.target, values = this.values, vlen = values.length, options = this.options, canvasWidth = this.canvasWidth, canvasHeight = this.canvasHeight, minValue = options.get('chartRangeMin') === undefined ? Math.min.apply(Math, values) : options.get('chartRangeMin'), maxValue = options.get('chartRangeMax') === undefined ? Math.max.apply(Math, values) : options.get('chartRangeMax'), canvasLeft = 0, lwhisker, loutlier, iqr, q1, q2, q3, rwhisker, routlier, i, size, unitSize; if (!box._super.render.call(this)) { return; } if (options.get('raw')) { if (options.get('showOutliers') && values.length > 5) { loutlier = values[0]; lwhisker = values[1]; q1 = values[2]; q2 = values[3]; q3 = values[4]; rwhisker = values[5]; routlier = values[6]; } else { lwhisker = values[0]; q1 = values[1]; q2 = values[2]; q3 = values[3]; rwhisker = values[4]; } } else { values.sort(function (a, b) { return a - b; }); q1 = quartile(values, 1); q2 = quartile(values, 2); q3 = quartile(values, 3); iqr = q3 - q1; if (options.get('showOutliers')) { lwhisker = rwhisker = undefined; for (i = 0; i < vlen; i++) { if (lwhisker === undefined && values[i] > q1 - (iqr * options.get('outlierIQR'))) { lwhisker = values[i]; } if (values[i] < q3 + (iqr * options.get('outlierIQR'))) { rwhisker = values[i]; } } loutlier = values[0]; routlier = values[vlen - 1]; } else { lwhisker = values[0]; rwhisker = values[vlen - 1]; } } this.quartiles = [q1, q2, q3]; this.lwhisker = lwhisker; this.rwhisker = rwhisker; this.loutlier = loutlier; this.routlier = routlier; unitSize = canvasWidth / (maxValue - minValue + 1); if (options.get('showOutliers')) { canvasLeft = Math.ceil(options.get('spotRadius')); canvasWidth -= 2 * Math.ceil(options.get('spotRadius')); unitSize = canvasWidth / (maxValue - minValue + 1); if (loutlier < lwhisker) { target.drawCircle((loutlier - minValue) * unitSize + canvasLeft, canvasHeight / 2, options.get('spotRadius'), options.get('outlierLineColor'), options.get('outlierFillColor')).append(); } if (routlier > rwhisker) { target.drawCircle((routlier - minValue) * unitSize + canvasLeft, canvasHeight / 2, options.get('spotRadius'), options.get('outlierLineColor'), options.get('outlierFillColor')).append(); } } // box target.drawRect( Math.round((q1 - minValue) * unitSize + canvasLeft), Math.round(canvasHeight * 0.1), Math.round((q3 - q1) * unitSize), Math.round(canvasHeight * 0.8), options.get('boxLineColor'), options.get('boxFillColor')).append(); // left whisker target.drawLine( Math.round((lwhisker - minValue) * unitSize + canvasLeft), Math.round(canvasHeight / 2), Math.round((q1 - minValue) * unitSize + canvasLeft), Math.round(canvasHeight / 2), options.get('lineColor')).append(); target.drawLine( Math.round((lwhisker - minValue) * unitSize + canvasLeft), Math.round(canvasHeight / 4), Math.round((lwhisker - minValue) * unitSize + canvasLeft), Math.round(canvasHeight - canvasHeight / 4), options.get('whiskerColor')).append(); // right whisker target.drawLine(Math.round((rwhisker - minValue) * unitSize + canvasLeft), Math.round(canvasHeight / 2), Math.round((q3 - minValue) * unitSize + canvasLeft), Math.round(canvasHeight / 2), options.get('lineColor')).append(); target.drawLine( Math.round((rwhisker - minValue) * unitSize + canvasLeft), Math.round(canvasHeight / 4), Math.round((rwhisker - minValue) * unitSize + canvasLeft), Math.round(canvasHeight - canvasHeight / 4), options.get('whiskerColor')).append(); // median line target.drawLine( Math.round((q2 - minValue) * unitSize + canvasLeft), Math.round(canvasHeight * 0.1), Math.round((q2 - minValue) * unitSize + canvasLeft), Math.round(canvasHeight * 0.9), options.get('medianColor')).append(); if (options.get('target')) { size = Math.ceil(options.get('spotRadius')); target.drawLine( Math.round((options.get('target') - minValue) * unitSize + canvasLeft), Math.round((canvasHeight / 2) - size), Math.round((options.get('target') - minValue) * unitSize + canvasLeft), Math.round((canvasHeight / 2) + size), options.get('targetColor')).append(); target.drawLine( Math.round((options.get('target') - minValue) * unitSize + canvasLeft - size), Math.round(canvasHeight / 2), Math.round((options.get('target') - minValue) * unitSize + canvasLeft + size), Math.round(canvasHeight / 2), options.get('targetColor')).append(); } target.render(); } }); // Setup a very simple "virtual canvas" to make drawing the few shapes we need easier // This is accessible as $(foo).simpledraw() VShape = createClass({ init: function (target, id, type, args) { this.target = target; this.id = id; this.type = type; this.args = args; }, append: function () { this.target.appendShape(this); return this; } }); VCanvas_base = createClass({ _pxregex: /(\d+)(px)?\s*$/i, init: function (width, height, target) { if (!width) { return; } this.width = width; this.height = height; this.target = target; this.lastShapeId = null; if (target[0]) { target = target[0]; } $.data(target, '_jqs_vcanvas', this); }, drawLine: function (x1, y1, x2, y2, lineColor, lineWidth) { return this.drawShape([[x1, y1], [x2, y2]], lineColor, lineWidth); }, drawShape: function (path, lineColor, fillColor, lineWidth) { return this._genShape('Shape', [path, lineColor, fillColor, lineWidth]); }, drawCircle: function (x, y, radius, lineColor, fillColor, lineWidth) { return this._genShape('Circle', [x, y, radius, lineColor, fillColor, lineWidth]); }, drawPieSlice: function (x, y, radius, startAngle, endAngle, lineColor, fillColor) { return this._genShape('PieSlice', [x, y, radius, startAngle, endAngle, lineColor, fillColor]); }, drawRect: function (x, y, width, height, lineColor, fillColor) { return this._genShape('Rect', [x, y, width, height, lineColor, fillColor]); }, getElement: function () { return this.canvas; }, /** * Return the most recently inserted shape id */ getLastShapeId: function () { return this.lastShapeId; }, /** * Clear and reset the canvas */ reset: function () { alert('reset not implemented'); }, _insert: function (el, target) { $(target).html(el); }, /** * Calculate the pixel dimensions of the canvas */ _calculatePixelDims: function (width, height, canvas) { // XXX This should probably be a configurable option var match; match = this._pxregex.exec(height); if (match) { this.pixelHeight = match[1]; } else { this.pixelHeight = $(canvas).height(); } match = this._pxregex.exec(width); if (match) { this.pixelWidth = match[1]; } else { this.pixelWidth = $(canvas).width(); } }, /** * Generate a shape object and id for later rendering */ _genShape: function (shapetype, shapeargs) { var id = shapeCount++; shapeargs.unshift(id); return new VShape(this, id, shapetype, shapeargs); }, /** * Add a shape to the end of the render queue */ appendShape: function (shape) { alert('appendShape not implemented'); }, /** * Replace one shape with another */ replaceWithShape: function (shapeid, shape) { alert('replaceWithShape not implemented'); }, /** * Insert one shape after another in the render queue */ insertAfterShape: function (shapeid, shape) { alert('insertAfterShape not implemented'); }, /** * Remove a shape from the queue */ removeShapeId: function (shapeid) { alert('removeShapeId not implemented'); }, /** * Find a shape at the specified x/y co-ordinates */ getShapeAt: function (el, x, y) { alert('getShapeAt not implemented'); }, /** * Render all queued shapes onto the canvas */ render: function () { alert('render not implemented'); } }); VCanvas_canvas = createClass(VCanvas_base, { init: function (width, height, target, interact) { VCanvas_canvas._super.init.call(this, width, height, target); this.canvas = document.createElement('canvas'); if (target[0]) { target = target[0]; } $.data(target, '_jqs_vcanvas', this); $(this.canvas).css({ display: 'inline-block', width: width, height: height, verticalAlign: 'top' }); this._insert(this.canvas, target); this._calculatePixelDims(width, height, this.canvas); this.canvas.width = this.pixelWidth; this.canvas.height = this.pixelHeight; this.interact = interact; this.shapes = {}; this.shapeseq = []; this.currentTargetShapeId = undefined; $(this.canvas).css({width: this.pixelWidth, height: this.pixelHeight}); }, _getContext: function (lineColor, fillColor, lineWidth) { var context = this.canvas.getContext('2d'); if (lineColor !== undefined) { context.strokeStyle = lineColor; } context.lineWidth = lineWidth === undefined ? 1 : lineWidth; if (fillColor !== undefined) { context.fillStyle = fillColor; } return context; }, reset: function () { var context = this._getContext(); context.clearRect(0, 0, this.pixelWidth, this.pixelHeight); this.shapes = {}; this.shapeseq = []; this.currentTargetShapeId = undefined; }, _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) { var context = this._getContext(lineColor, fillColor, lineWidth), i, plen; context.beginPath(); context.moveTo(path[0][0] + 0.5, path[0][1] + 0.5); for (i = 1, plen = path.length; i < plen; i++) { context.lineTo(path[i][0] + 0.5, path[i][1] + 0.5); // the 0.5 offset gives us crisp pixel-width lines } if (lineColor !== undefined) { context.stroke(); } if (fillColor !== undefined) { context.fill(); } if (this.targetX !== undefined && this.targetY !== undefined && context.isPointInPath(this.targetX, this.targetY)) { this.currentTargetShapeId = shapeid; } }, _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) { var context = this._getContext(lineColor, fillColor, lineWidth); context.beginPath(); context.arc(x, y, radius, 0, 2 * Math.PI, false); if (this.targetX !== undefined && this.targetY !== undefined && context.isPointInPath(this.targetX, this.targetY)) { this.currentTargetShapeId = shapeid; } if (lineColor !== undefined) { context.stroke(); } if (fillColor !== undefined) { context.fill(); } }, _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) { var context = this._getContext(lineColor, fillColor); context.beginPath(); context.moveTo(x, y); context.arc(x, y, radius, startAngle, endAngle, false); context.lineTo(x, y); context.closePath(); if (lineColor !== undefined) { context.stroke(); } if (fillColor) { context.fill(); } if (this.targetX !== undefined && this.targetY !== undefined && context.isPointInPath(this.targetX, this.targetY)) { this.currentTargetShapeId = shapeid; } }, _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) { return this._drawShape(shapeid, [[x, y], [x + width, y], [x + width, y + height], [x, y + height], [x, y]], lineColor, fillColor); }, appendShape: function (shape) { this.shapes[shape.id] = shape; this.shapeseq.push(shape.id); this.lastShapeId = shape.id; return shape.id; }, replaceWithShape: function (shapeid, shape) { var shapeseq = this.shapeseq, i; this.shapes[shape.id] = shape; for (i = shapeseq.length; i--;) { if (shapeseq[i] == shapeid) { shapeseq[i] = shape.id; } } delete this.shapes[shapeid]; }, replaceWithShapes: function (shapeids, shapes) { var shapeseq = this.shapeseq, shapemap = {}, sid, i, first; for (i = shapeids.length; i--;) { shapemap[shapeids[i]] = true; } for (i = shapeseq.length; i--;) { sid = shapeseq[i]; if (shapemap[sid]) { shapeseq.splice(i, 1); delete this.shapes[sid]; first = i; } } for (i = shapes.length; i--;) { shapeseq.splice(first, 0, shapes[i].id); this.shapes[shapes[i].id] = shapes[i]; } }, insertAfterShape: function (shapeid, shape) { var shapeseq = this.shapeseq, i; for (i = shapeseq.length; i--;) { if (shapeseq[i] === shapeid) { shapeseq.splice(i + 1, 0, shape.id); this.shapes[shape.id] = shape; return; } } }, removeShapeId: function (shapeid) { var shapeseq = this.shapeseq, i; for (i = shapeseq.length; i--;) { if (shapeseq[i] === shapeid) { shapeseq.splice(i, 1); break; } } delete this.shapes[shapeid]; }, getShapeAt: function (el, x, y) { this.targetX = x; this.targetY = y; this.render(); return this.currentTargetShapeId; }, render: function () { var shapeseq = this.shapeseq, shapes = this.shapes, shapeCount = shapeseq.length, context = this._getContext(), shapeid, shape, i; context.clearRect(0, 0, this.pixelWidth, this.pixelHeight); for (i = 0; i < shapeCount; i++) { shapeid = shapeseq[i]; shape = shapes[shapeid]; this['_draw' + shape.type].apply(this, shape.args); } if (!this.interact) { // not interactive so no need to keep the shapes array this.shapes = {}; this.shapeseq = []; } } }); VCanvas_vml = createClass(VCanvas_base, { init: function (width, height, target) { var groupel; VCanvas_vml._super.init.call(this, width, height, target); if (target[0]) { target = target[0]; } $.data(target, '_jqs_vcanvas', this); this.canvas = document.createElement('span'); $(this.canvas).css({ display: 'inline-block', position: 'relative', overflow: 'hidden', width: width, height: height, margin: '0px', padding: '0px', verticalAlign: 'top'}); this._insert(this.canvas, target); this._calculatePixelDims(width, height, this.canvas); this.canvas.width = this.pixelWidth; this.canvas.height = this.pixelHeight; groupel = '<v:group coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '"' + ' style="position:absolute;top:0;left:0;width:' + this.pixelWidth + 'px;height=' + this.pixelHeight + 'px;"></v:group>'; this.canvas.insertAdjacentHTML('beforeEnd', groupel); this.group = $(this.canvas).children()[0]; this.rendered = false; this.prerender = ''; }, _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) { var vpath = [], initial, stroke, fill, closed, vel, plen, i; for (i = 0, plen = path.length; i < plen; i++) { vpath[i] = '' + (path[i][0]) + ',' + (path[i][1]); } initial = vpath.splice(0, 1); lineWidth = lineWidth === undefined ? 1 : lineWidth; stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" '; fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" '; closed = vpath[0] === vpath[vpath.length - 1] ? 'x ' : ''; vel = '<v:shape coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '" ' + ' id="jqsshape' + shapeid + '" ' + stroke + fill + ' style="position:absolute;left:0px;top:0px;height:' + this.pixelHeight + 'px;width:' + this.pixelWidth + 'px;padding:0px;margin:0px;" ' + ' path="m ' + initial + ' l ' + vpath.join(', ') + ' ' + closed + 'e">' + ' </v:shape>'; return vel; }, _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) { var stroke, fill, vel; x -= radius; y -= radius; stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" '; fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" '; vel = '<v:oval ' + ' id="jqsshape' + shapeid + '" ' + stroke + fill + ' style="position:absolute;top:' + y + 'px; left:' + x + 'px; width:' + (radius * 2) + 'px; height:' + (radius * 2) + 'px"></v:oval>'; return vel; }, _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) { var vpath, startx, starty, endx, endy, stroke, fill, vel; if (startAngle === endAngle) { return ''; // VML seems to have problem when start angle equals end angle. } if ((endAngle - startAngle) === (2 * Math.PI)) { startAngle = 0.0; // VML seems to have a problem when drawing a full circle that doesn't start 0 endAngle = (2 * Math.PI); } startx = x + Math.round(Math.cos(startAngle) * radius); starty = y + Math.round(Math.sin(startAngle) * radius); endx = x + Math.round(Math.cos(endAngle) * radius); endy = y + Math.round(Math.sin(endAngle) * radius); if (startx === endx && starty === endy) { if ((endAngle - startAngle) < Math.PI) { // Prevent very small slices from being mistaken as a whole pie return ''; } // essentially going to be the entire circle, so ignore startAngle startx = endx = x + radius; starty = endy = y; } if (startx === endx && starty === endy && (endAngle - startAngle) < Math.PI) { return ''; } vpath = [x - radius, y - radius, x + radius, y + radius, startx, starty, endx, endy]; stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="1px" strokeColor="' + lineColor + '" '; fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" '; vel = '<v:shape coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '" ' + ' id="jqsshape' + shapeid + '" ' + stroke + fill + ' style="position:absolute;left:0px;top:0px;height:' + this.pixelHeight + 'px;width:' + this.pixelWidth + 'px;padding:0px;margin:0px;" ' + ' path="m ' + x + ',' + y + ' wa ' + vpath.join(', ') + ' x e">' + ' </v:shape>'; return vel; }, _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) { return this._drawShape(shapeid, [[x, y], [x, y + height], [x + width, y + height], [x + width, y], [x, y]], lineColor, fillColor); }, reset: function () { this.group.innerHTML = ''; }, appendShape: function (shape) { var vel = this['_draw' + shape.type].apply(this, shape.args); if (this.rendered) { this.group.insertAdjacentHTML('beforeEnd', vel); } else { this.prerender += vel; } this.lastShapeId = shape.id; return shape.id; }, replaceWithShape: function (shapeid, shape) { var existing = $('#jqsshape' + shapeid), vel = this['_draw' + shape.type].apply(this, shape.args); existing[0].outerHTML = vel; }, replaceWithShapes: function (shapeids, shapes) { // replace the first shapeid with all the new shapes then toast the remaining old shapes var existing = $('#jqsshape' + shapeids[0]), replace = '', slen = shapes.length, i; for (i = 0; i < slen; i++) { replace += this['_draw' + shapes[i].type].apply(this, shapes[i].args); } existing[0].outerHTML = replace; for (i = 1; i < shapeids.length; i++) { $('#jqsshape' + shapeids[i]).remove(); } }, insertAfterShape: function (shapeid, shape) { var existing = $('#jqsshape' + shapeid), vel = this['_draw' + shape.type].apply(this, shape.args); existing[0].insertAdjacentHTML('afterEnd', vel); }, removeShapeId: function (shapeid) { var existing = $('#jqsshape' + shapeid); this.group.removeChild(existing[0]); }, getShapeAt: function (el, x, y) { var shapeid = el.id.substr(8); return shapeid; }, render: function () { if (!this.rendered) { // batch the intial render into a single repaint this.group.innerHTML = this.prerender; this.rendered = true; } } }); }))}(document, Math)); piegage/piegage-demo.js 0000644 00000006667 15030376016 0011046 0 ustar 00 /* Pie gauges */ var initPieChart = function() { $('.chart').easyPieChart({ barColor: function(percent) { percent /= 100; return "rgb(" + Math.round(254 * (1 - percent)) + ", " + Math.round(255 * percent) + ", 0)"; }, animate: 1000, scaleColor: '#ccc', lineWidth: 3, size: 100, lineCap: 'cap', onStep: function(value) { this.$el.find('span').text(~~value); } }); $('.chart-home').easyPieChart({ barColor: 'rgba(255,255,255,0.5)', trackColor: 'rgba(255,255,255,0.1)', animate: 1000, scaleColor: 'rgba(255,255,255,0.3)', lineWidth: 3, size: 100, lineCap: 'cap', onStep: function(value) { this.$el.find('span').text(~~value); } }); $('.chart-alt').easyPieChart({ barColor: function(percent) { percent /= 100; return "rgb(" + Math.round(255 * (1 - percent)) + ", " + Math.round(255 * percent) + ", 0)"; }, trackColor: '#333', scaleColor: false, lineCap: 'butt', rotate: -90, lineWidth: 20, animate: 1500, onStep: function(value) { this.$el.find('span').text(~~value); } }); $('.chart-alt-1').easyPieChart({ barColor: function(percent) { percent /= 100; return "rgb(" + Math.round(255 * (1 - percent)) + ", " + Math.round(255 * percent) + ", 0)"; }, trackColor: '#e1ecf1', scaleColor: '#c4d7e0', lineCap: 'cap', rotate: -90, lineWidth: 10, size: 80, animate: 2500, onStep: function(value) { this.$el.find('span').text(~~value); } }); $('.chart-alt-2').easyPieChart({ barColor: function(percent) { percent /= 100; return "rgb(" + Math.round(255 * (1 - percent)) + ", " + Math.round(255 * percent) + ", 0)"; }, trackColor: '#fff', scaleColor: false, lineCap: 'butt', rotate: -90, lineWidth: 4, size: 50, animate: 1500, onStep: function(value) { this.$el.find('span').text(~~value); } }); $('.chart-alt-3').easyPieChart({ barColor: function(percent) { percent /= 100; return "rgb(" + Math.round(255 * (1 - percent)) + ", " + Math.round(255 * percent) + ", 0)"; }, trackColor: '#333', scaleColor: true, lineCap: 'butt', rotate: -90, lineWidth: 4, size: 50, animate: 1500, onStep: function(value) { this.$el.find('span').text(~~value); } }); $('.chart-alt-10').easyPieChart({ barColor: 'rgba(255,255,255,255.4)', trackColor: 'rgba(255,255,255,0.1)', scaleColor: 'transparent', lineCap: 'round', rotate: -90, lineWidth: 4, size: 100, animate: 2500, onStep: function(value) { this.$el.find('span').text(~~value); } }); $('.updateEasyPieChart').on('click', function(e) { e.preventDefault(); $('.chart-home, .chart, .chart-alt, .chart-alt-1, .chart-alt-2, .chart-alt-3, .chart-alt-10').each(function() { $(this).data('easyPieChart').update(Math.round(100 * Math.random())); }); }); }; $(document).ready(function() { initPieChart(); }); piegage/piegage.js 0000644 00000016554 15030376016 0010120 0 ustar 00 // Generated by CoffeeScript 1.6.2 /* Easy pie chart is a jquery plugin to display simple animated pie charts for only one value Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. Built on top of the jQuery library (http://jquery.com) @source: http://github.com/rendro/easy-pie-chart/ @autor: Robert Fleischmann @version: 1.2.1 Inspired by: http://dribbble.com/shots/631074-Simple-Pie-Charts-II?list=popular&offset=210 Thanks to Philip Thrasher for the jquery plugin boilerplate for coffee script */ (function($) { $.easyPieChart = function(el, options) { var addScaleLine, animateLine, drawLine, easeInOutQuad, rAF, renderBackground, renderScale, renderTrack, _this = this; this.el = el; this.$el = $(el); this.$el.data("easyPieChart", this); this.init = function() { var percent, scaleBy; _this.options = $.extend({}, $.easyPieChart.defaultOptions, options); percent = parseInt(_this.$el.data('percent'), 10); _this.percentage = 0; _this.canvas = $("<canvas width='" + _this.options.size + "' height='" + _this.options.size + "'></canvas>").get(0); _this.$el.append(_this.canvas); if (typeof G_vmlCanvasManager !== "undefined" && G_vmlCanvasManager !== null) { G_vmlCanvasManager.initElement(_this.canvas); } _this.ctx = _this.canvas.getContext('2d'); if (window.devicePixelRatio > 1) { scaleBy = window.devicePixelRatio; $(_this.canvas).css({ width: _this.options.size, height: _this.options.size }); _this.canvas.width *= scaleBy; _this.canvas.height *= scaleBy; _this.ctx.scale(scaleBy, scaleBy); } _this.ctx.translate(_this.options.size / 2, _this.options.size / 2); _this.ctx.rotate(_this.options.rotate * Math.PI / 180); _this.$el.addClass('easyPieChart'); _this.$el.css({ width: _this.options.size, height: _this.options.size, lineHeight: "" + _this.options.size + "px" }); _this.update(percent); return _this; }; this.update = function(percent) { percent = parseFloat(percent) || 0; if (_this.options.animate === false) { drawLine(percent); } else { animateLine(_this.percentage, percent); } return _this; }; renderScale = function() { var i, _i, _results; _this.ctx.fillStyle = _this.options.scaleColor; _this.ctx.lineWidth = 1; _results = []; for (i = _i = 0; _i <= 24; i = ++_i) { _results.push(addScaleLine(i)); } return _results; }; addScaleLine = function(i) { var offset; offset = i % 6 === 0 ? 0 : _this.options.size * 0.017; _this.ctx.save(); _this.ctx.rotate(i * Math.PI / 12); _this.ctx.fillRect(_this.options.size / 2 - offset, 0, -_this.options.size * 0.05 + offset, 1); _this.ctx.restore(); }; renderTrack = function() { var offset; offset = _this.options.size / 2 - _this.options.lineWidth / 2; if (_this.options.scaleColor !== false) { offset -= _this.options.size * 0.08; } _this.ctx.beginPath(); _this.ctx.arc(0, 0, offset, 0, Math.PI * 2, true); _this.ctx.closePath(); _this.ctx.strokeStyle = _this.options.trackColor; _this.ctx.lineWidth = _this.options.lineWidth; _this.ctx.stroke(); }; renderBackground = function() { if (_this.options.scaleColor !== false) { renderScale(); } if (_this.options.trackColor !== false) { renderTrack(); } }; drawLine = function(percent) { var offset; renderBackground(); _this.ctx.strokeStyle = $.isFunction(_this.options.barColor) ? _this.options.barColor(percent) : _this.options.barColor; _this.ctx.lineCap = _this.options.lineCap; _this.ctx.lineWidth = _this.options.lineWidth; offset = _this.options.size / 2 - _this.options.lineWidth / 2; if (_this.options.scaleColor !== false) { offset -= _this.options.size * 0.08; } _this.ctx.save(); _this.ctx.rotate(-Math.PI / 2); _this.ctx.beginPath(); _this.ctx.arc(0, 0, offset, 0, Math.PI * 2 * percent / 100, false); _this.ctx.stroke(); _this.ctx.restore(); }; rAF = (function() { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { return window.setTimeout(callback, 1000 / 60); }; })(); animateLine = function(from, to) { var anim, startTime; _this.options.onStart.call(_this); _this.percentage = to; startTime = Date.now(); anim = function() { var currentValue, process; process = Date.now() - startTime; if (process < _this.options.animate) { rAF(anim); } _this.ctx.clearRect(-_this.options.size / 2, -_this.options.size / 2, _this.options.size, _this.options.size); renderBackground.call(_this); currentValue = [easeInOutQuad(process, from, to - from, _this.options.animate)]; _this.options.onStep.call(_this, currentValue); drawLine.call(_this, currentValue); if (process >= _this.options.animate) { return _this.options.onStop.call(_this); } }; rAF(anim); }; easeInOutQuad = function(t, b, c, d) { var easeIn, easing; easeIn = function(t) { return Math.pow(t, 2); }; easing = function(t) { if (t < 1) { return easeIn(t); } else { return 2 - easeIn((t / 2) * -2 + 2); } }; t /= d / 2; return c / 2 * easing(t) + b; }; return this.init(); }; $.easyPieChart.defaultOptions = { barColor: '#ef1e25', trackColor: '#f2f2f2', scaleColor: '#dfe0e0', lineCap: 'round', rotate: 0, size: 110, lineWidth: 3, animate: false, onStart: $.noop, onStop: $.noop, onStep: $.noop }; $.fn.easyPieChart = function(options) { return $.each(this, function(i, el) { var $el, instanceOptions; $el = $(el); if (!$el.data('easyPieChart')) { instanceOptions = $.extend({}, options, $el.data()); return $el.data('easyPieChart', new $.easyPieChart(el, instanceOptions)); } }); }; return void 0; })(jQuery); piegage/piegage.css 0000644 00000001013 15030376016 0010254 0 ustar 00 /* Pie gauge */ .easyPieChart { position: relative; text-align: center; margin-left: auto; margin-right: auto; } .easyPieChart canvas { position: absolute; top: 0; left: 0; } .chart-home, .chart, .chart-alt, .chart-alt-1, .chart-alt-2, .chart-alt-3 { text-align: center; font-weight: bold; margin: 0 auto; } /* Flot charts */ .chart-wrapper { width: 100%; height: 350px; } .chart-container { width: 100%; height: 100%; font-size: 14px; line-height: 1.2em; } chart-js/chart-bar.js 0000644 00000022232 15030376016 0010462 0 ustar 00 (function(){ "use strict"; var root = this, Chart = root.Chart, helpers = Chart.helpers; var defaultConfig = { //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value scaleBeginAtZero : true, //Boolean - Whether grid lines are shown across the chart scaleShowGridLines : true, //String - Colour of the grid lines scaleGridLineColor : "rgba(0,0,0,.05)", //Number - Width of the grid lines scaleGridLineWidth : 1, //Boolean - If there is a stroke on each bar barShowStroke : true, //Number - Pixel width of the bar stroke barStrokeWidth : 2, //Number - Spacing between each of the X value sets barValueSpacing : 5, //Number - Spacing between data sets within X values barDatasetSpacing : 1, //String - A legend template legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].fillColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>" }; Chart.Type.extend({ name: "Bar", defaults : defaultConfig, initialize: function(data){ //Expose options as a scope variable here so we can access it in the ScaleClass var options = this.options; this.ScaleClass = Chart.Scale.extend({ offsetGridLines : true, calculateBarX : function(datasetCount, datasetIndex, barIndex){ //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar var xWidth = this.calculateBaseWidth(), xAbsolute = this.calculateX(barIndex) - (xWidth/2), barWidth = this.calculateBarWidth(datasetCount); return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; }, calculateBaseWidth : function(){ return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); }, calculateBarWidth : function(datasetCount){ //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); return (baseWidth / datasetCount); } }); this.datasets = []; //Set up tooltip events on the chart if (this.options.showTooltips){ helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; this.eachBars(function(bar){ bar.restore(['fillColor', 'strokeColor']); }); helpers.each(activeBars, function(activeBar){ activeBar.fillColor = activeBar.highlightFill; activeBar.strokeColor = activeBar.highlightStroke; }); this.showTooltip(activeBars); }); } //Declare the extension of the default point, to cater for the options passed in to the constructor this.BarClass = Chart.Rectangle.extend({ strokeWidth : this.options.barStrokeWidth, showStroke : this.options.barShowStroke, ctx : this.chart.ctx }); //Iterate through each of the datasets, and build this into a property of the chart helpers.each(data.datasets,function(dataset,datasetIndex){ var datasetObject = { label : dataset.label || null, fillColor : dataset.fillColor, strokeColor : dataset.strokeColor, bars : [] }; this.datasets.push(datasetObject); helpers.each(dataset.data,function(dataPoint,index){ //Add a new point for each piece of data, passing any required data to draw. datasetObject.bars.push(new this.BarClass({ value : dataPoint, label : data.labels[index], datasetLabel: dataset.label, strokeColor : dataset.strokeColor, fillColor : dataset.fillColor, highlightFill : dataset.highlightFill || dataset.fillColor, highlightStroke : dataset.highlightStroke || dataset.strokeColor })); },this); },this); this.buildScale(data.labels); this.BarClass.prototype.base = this.scale.endPoint; this.eachBars(function(bar, index, datasetIndex){ helpers.extend(bar, { width : this.scale.calculateBarWidth(this.datasets.length), x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index), y: this.scale.endPoint }); bar.save(); }, this); this.render(); }, update : function(){ this.scale.update(); // Reset any highlight colours before updating. helpers.each(this.activeElements, function(activeElement){ activeElement.restore(['fillColor', 'strokeColor']); }); this.eachBars(function(bar){ bar.save(); }); this.render(); }, eachBars : function(callback){ helpers.each(this.datasets,function(dataset, datasetIndex){ helpers.each(dataset.bars, callback, this, datasetIndex); },this); }, getBarsAtEvent : function(e){ var barsArray = [], eventPosition = helpers.getRelativePosition(e), datasetIterator = function(dataset){ barsArray.push(dataset.bars[barIndex]); }, barIndex; for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ helpers.each(this.datasets, datasetIterator); return barsArray; } } } return barsArray; }, buildScale : function(labels){ var self = this; var dataTotal = function(){ var values = []; self.eachBars(function(bar){ values.push(bar.value); }); return values; }; var scaleOptions = { templateString : this.options.scaleLabel, height : this.chart.height, width : this.chart.width, ctx : this.chart.ctx, textColor : this.options.scaleFontColor, fontSize : this.options.scaleFontSize, fontStyle : this.options.scaleFontStyle, fontFamily : this.options.scaleFontFamily, valuesCount : labels.length, beginAtZero : this.options.scaleBeginAtZero, integersOnly : this.options.scaleIntegersOnly, calculateYRange: function(currentHeight){ var updatedRanges = helpers.calculateScaleRange( dataTotal(), currentHeight, this.fontSize, this.beginAtZero, this.integersOnly ); helpers.extend(this, updatedRanges); }, xLabels : labels, font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), lineWidth : this.options.scaleLineWidth, lineColor : this.options.scaleLineColor, gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, showLabels : this.options.scaleShowLabels, display : this.options.showScale }; if (this.options.scaleOverride){ helpers.extend(scaleOptions, { calculateYRange: helpers.noop, steps: this.options.scaleSteps, stepValue: this.options.scaleStepWidth, min: this.options.scaleStartValue, max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) }); } this.scale = new this.ScaleClass(scaleOptions); }, addData : function(valuesArray,label){ //Map the values array for each of the datasets helpers.each(valuesArray,function(value,datasetIndex){ //Add a new point for each piece of data, passing any required data to draw. this.datasets[datasetIndex].bars.push(new this.BarClass({ value : value, label : label, x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), y: this.scale.endPoint, width : this.scale.calculateBarWidth(this.datasets.length), base : this.scale.endPoint, strokeColor : this.datasets[datasetIndex].strokeColor, fillColor : this.datasets[datasetIndex].fillColor })); },this); this.scale.addXLabel(label); //Then re-render the chart. this.update(); }, removeData : function(){ this.scale.removeXLabel(); //Then re-render the chart. helpers.each(this.datasets,function(dataset){ dataset.bars.shift(); },this); this.update(); }, reflow : function(){ helpers.extend(this.BarClass.prototype,{ y: this.scale.endPoint, base : this.scale.endPoint }); var newScaleProps = helpers.extend({ height : this.chart.height, width : this.chart.width }); this.scale.update(newScaleProps); }, draw : function(ease){ var easingDecimal = ease || 1; this.clear(); var ctx = this.chart.ctx; this.scale.draw(easingDecimal); //Draw all the bars for each dataset helpers.each(this.datasets,function(dataset,datasetIndex){ helpers.each(dataset.bars,function(bar,index){ if (bar.hasValue()){ bar.base = this.scale.endPoint; //Transition then draw bar.transition({ x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), y : this.scale.calculateY(bar.value), width : this.scale.calculateBarWidth(this.datasets.length) }, easingDecimal).draw(); } },this); },this); } }); }).call(this); chart-js/chart-radar.js 0000644 00000023551 15030376016 0011014 0 ustar 00 (function(){ "use strict"; var root = this, Chart = root.Chart, helpers = Chart.helpers; Chart.Type.extend({ name: "Radar", defaults:{ //Boolean - Whether to show lines for each scale point scaleShowLine : true, //Boolean - Whether we show the angle lines out of the radar angleShowLineOut : true, //Boolean - Whether to show labels on the scale scaleShowLabels : false, // Boolean - Whether the scale should begin at zero scaleBeginAtZero : true, //String - Colour of the angle line angleLineColor : "rgba(0,0,0,.1)", //Number - Pixel width of the angle line angleLineWidth : 1, //String - Point label font declaration pointLabelFontFamily : "'Arial'", //String - Point label font weight pointLabelFontStyle : "normal", //Number - Point label font size in pixels pointLabelFontSize : 10, //String - Point label font colour pointLabelFontColor : "#666", //Boolean - Whether to show a dot for each point pointDot : true, //Number - Radius of each point dot in pixels pointDotRadius : 3, //Number - Pixel width of point dot stroke pointDotStrokeWidth : 1, //Number - amount extra to add to the radius to cater for hit detection outside the drawn point pointHitDetectionRadius : 20, //Boolean - Whether to show a stroke for datasets datasetStroke : true, //Number - Pixel width of dataset stroke datasetStrokeWidth : 2, //Boolean - Whether to fill the dataset with a colour datasetFill : true, //String - A legend template legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>" }, initialize: function(data){ this.PointClass = Chart.Point.extend({ strokeWidth : this.options.pointDotStrokeWidth, radius : this.options.pointDotRadius, display: this.options.pointDot, hitDetectionRadius : this.options.pointHitDetectionRadius, ctx : this.chart.ctx }); this.datasets = []; this.buildScale(data); //Set up tooltip events on the chart if (this.options.showTooltips){ helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; this.eachPoints(function(point){ point.restore(['fillColor', 'strokeColor']); }); helpers.each(activePointsCollection, function(activePoint){ activePoint.fillColor = activePoint.highlightFill; activePoint.strokeColor = activePoint.highlightStroke; }); this.showTooltip(activePointsCollection); }); } //Iterate through each of the datasets, and build this into a property of the chart helpers.each(data.datasets,function(dataset){ var datasetObject = { label: dataset.label || null, fillColor : dataset.fillColor, strokeColor : dataset.strokeColor, pointColor : dataset.pointColor, pointStrokeColor : dataset.pointStrokeColor, points : [] }; this.datasets.push(datasetObject); helpers.each(dataset.data,function(dataPoint,index){ //Add a new point for each piece of data, passing any required data to draw. var pointPosition; if (!this.scale.animation){ pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); } datasetObject.points.push(new this.PointClass({ value : dataPoint, label : data.labels[index], datasetLabel: dataset.label, x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, strokeColor : dataset.pointStrokeColor, fillColor : dataset.pointColor, highlightFill : dataset.pointHighlightFill || dataset.pointColor, highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor })); },this); },this); this.render(); }, eachPoints : function(callback){ helpers.each(this.datasets,function(dataset){ helpers.each(dataset.points,callback,this); },this); }, getPointsAtEvent : function(evt){ var mousePosition = helpers.getRelativePosition(evt), fromCenter = helpers.getAngleFromPoint({ x: this.scale.xCenter, y: this.scale.yCenter }, mousePosition); var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), activePointsCollection = []; // If we're at the top, make the pointIndex 0 to get the first of the array. if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ pointIndex = 0; } if (fromCenter.distance <= this.scale.drawingArea){ helpers.each(this.datasets, function(dataset){ activePointsCollection.push(dataset.points[pointIndex]); }); } return activePointsCollection; }, buildScale : function(data){ this.scale = new Chart.RadialScale({ display: this.options.showScale, fontStyle: this.options.scaleFontStyle, fontSize: this.options.scaleFontSize, fontFamily: this.options.scaleFontFamily, fontColor: this.options.scaleFontColor, showLabels: this.options.scaleShowLabels, showLabelBackdrop: this.options.scaleShowLabelBackdrop, backdropColor: this.options.scaleBackdropColor, backdropPaddingY : this.options.scaleBackdropPaddingY, backdropPaddingX: this.options.scaleBackdropPaddingX, lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, lineColor: this.options.scaleLineColor, angleLineColor : this.options.angleLineColor, angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, // Point labels at the edge of each line pointLabelFontColor : this.options.pointLabelFontColor, pointLabelFontSize : this.options.pointLabelFontSize, pointLabelFontFamily : this.options.pointLabelFontFamily, pointLabelFontStyle : this.options.pointLabelFontStyle, height : this.chart.height, width: this.chart.width, xCenter: this.chart.width/2, yCenter: this.chart.height/2, ctx : this.chart.ctx, templateString: this.options.scaleLabel, labels: data.labels, valuesCount: data.datasets[0].data.length }); this.scale.setScaleSize(); this.updateScaleRange(data.datasets); this.scale.buildYLabels(); }, updateScaleRange: function(datasets){ var valuesArray = (function(){ var totalDataArray = []; helpers.each(datasets,function(dataset){ if (dataset.data){ totalDataArray = totalDataArray.concat(dataset.data); } else { helpers.each(dataset.points, function(point){ totalDataArray.push(point.value); }); } }); return totalDataArray; })(); var scaleSizes = (this.options.scaleOverride) ? { steps: this.options.scaleSteps, stepValue: this.options.scaleStepWidth, min: this.options.scaleStartValue, max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) } : helpers.calculateScaleRange( valuesArray, helpers.min([this.chart.width, this.chart.height])/2, this.options.scaleFontSize, this.options.scaleBeginAtZero, this.options.scaleIntegersOnly ); helpers.extend( this.scale, scaleSizes ); }, addData : function(valuesArray,label){ //Map the values array for each of the datasets this.scale.valuesCount++; helpers.each(valuesArray,function(value,datasetIndex){ var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); this.datasets[datasetIndex].points.push(new this.PointClass({ value : value, label : label, x: pointPosition.x, y: pointPosition.y, strokeColor : this.datasets[datasetIndex].pointStrokeColor, fillColor : this.datasets[datasetIndex].pointColor })); },this); this.scale.labels.push(label); this.reflow(); this.update(); }, removeData : function(){ this.scale.valuesCount--; this.scale.labels.shift(); helpers.each(this.datasets,function(dataset){ dataset.points.shift(); },this); this.reflow(); this.update(); }, update : function(){ this.eachPoints(function(point){ point.save(); }); this.reflow(); this.render(); }, reflow: function(){ helpers.extend(this.scale, { width : this.chart.width, height: this.chart.height, size : helpers.min([this.chart.width, this.chart.height]), xCenter: this.chart.width/2, yCenter: this.chart.height/2 }); this.updateScaleRange(this.datasets); this.scale.setScaleSize(); this.scale.buildYLabels(); }, draw : function(ease){ var easeDecimal = ease || 1, ctx = this.chart.ctx; this.clear(); this.scale.draw(); helpers.each(this.datasets,function(dataset){ //Transition each point first so that the line and point drawing isn't out of sync helpers.each(dataset.points,function(point,index){ if (point.hasValue()){ point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); } },this); //Draw the line between all the points ctx.lineWidth = this.options.datasetStrokeWidth; ctx.strokeStyle = dataset.strokeColor; ctx.beginPath(); helpers.each(dataset.points,function(point,index){ if (index === 0){ ctx.moveTo(point.x,point.y); } else{ ctx.lineTo(point.x,point.y); } },this); ctx.closePath(); ctx.stroke(); ctx.fillStyle = dataset.fillColor; ctx.fill(); //Now draw the points over the line //A little inefficient double looping, but better than the line //lagging behind the point positions helpers.each(dataset.points,function(point){ if (point.hasValue()){ point.draw(); } }); },this); } }); }).call(this); chart-js/chart-line.js 0000644 00000026315 15030376016 0010653 0 ustar 00 (function(){ "use strict"; var root = this, Chart = root.Chart, helpers = Chart.helpers; var defaultConfig = { ///Boolean - Whether grid lines are shown across the chart scaleShowGridLines : true, //String - Colour of the grid lines scaleGridLineColor : "rgba(0,0,0,.05)", //Number - Width of the grid lines scaleGridLineWidth : 1, //Boolean - Whether the line is curved between points bezierCurve : true, //Number - Tension of the bezier curve between points bezierCurveTension : 0.4, //Boolean - Whether to show a dot for each point pointDot : true, //Number - Radius of each point dot in pixels pointDotRadius : 4, //Number - Pixel width of point dot stroke pointDotStrokeWidth : 1, //Number - amount extra to add to the radius to cater for hit detection outside the drawn point pointHitDetectionRadius : 20, //Boolean - Whether to show a stroke for datasets datasetStroke : true, //Number - Pixel width of dataset stroke datasetStrokeWidth : 2, //Boolean - Whether to fill the dataset with a colour datasetFill : true, //String - A legend template legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>" }; Chart.Type.extend({ name: "Line", defaults : defaultConfig, initialize: function(data){ //Declare the extension of the default point, to cater for the options passed in to the constructor this.PointClass = Chart.Point.extend({ strokeWidth : this.options.pointDotStrokeWidth, radius : this.options.pointDotRadius, display: this.options.pointDot, hitDetectionRadius : this.options.pointHitDetectionRadius, ctx : this.chart.ctx, inRange : function(mouseX){ return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); } }); this.datasets = []; //Set up tooltip events on the chart if (this.options.showTooltips){ helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; this.eachPoints(function(point){ point.restore(['fillColor', 'strokeColor']); }); helpers.each(activePoints, function(activePoint){ activePoint.fillColor = activePoint.highlightFill; activePoint.strokeColor = activePoint.highlightStroke; }); this.showTooltip(activePoints); }); } //Iterate through each of the datasets, and build this into a property of the chart helpers.each(data.datasets,function(dataset){ var datasetObject = { label : dataset.label || null, fillColor : dataset.fillColor, strokeColor : dataset.strokeColor, pointColor : dataset.pointColor, pointStrokeColor : dataset.pointStrokeColor, points : [] }; this.datasets.push(datasetObject); helpers.each(dataset.data,function(dataPoint,index){ //Add a new point for each piece of data, passing any required data to draw. datasetObject.points.push(new this.PointClass({ value : dataPoint, label : data.labels[index], datasetLabel: dataset.label, strokeColor : dataset.pointStrokeColor, fillColor : dataset.pointColor, highlightFill : dataset.pointHighlightFill || dataset.pointColor, highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor })); },this); this.buildScale(data.labels); this.eachPoints(function(point, index){ helpers.extend(point, { x: this.scale.calculateX(index), y: this.scale.endPoint }); point.save(); }, this); },this); this.render(); }, update : function(){ this.scale.update(); // Reset any highlight colours before updating. helpers.each(this.activeElements, function(activeElement){ activeElement.restore(['fillColor', 'strokeColor']); }); this.eachPoints(function(point){ point.save(); }); this.render(); }, eachPoints : function(callback){ helpers.each(this.datasets,function(dataset){ helpers.each(dataset.points,callback,this); },this); }, getPointsAtEvent : function(e){ var pointsArray = [], eventPosition = helpers.getRelativePosition(e); helpers.each(this.datasets,function(dataset){ helpers.each(dataset.points,function(point){ if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); }); },this); return pointsArray; }, buildScale : function(labels){ var self = this; var dataTotal = function(){ var values = []; self.eachPoints(function(point){ values.push(point.value); }); return values; }; var scaleOptions = { templateString : this.options.scaleLabel, height : this.chart.height, width : this.chart.width, ctx : this.chart.ctx, textColor : this.options.scaleFontColor, fontSize : this.options.scaleFontSize, fontStyle : this.options.scaleFontStyle, fontFamily : this.options.scaleFontFamily, valuesCount : labels.length, beginAtZero : this.options.scaleBeginAtZero, integersOnly : this.options.scaleIntegersOnly, calculateYRange : function(currentHeight){ var updatedRanges = helpers.calculateScaleRange( dataTotal(), currentHeight, this.fontSize, this.beginAtZero, this.integersOnly ); helpers.extend(this, updatedRanges); }, xLabels : labels, font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), lineWidth : this.options.scaleLineWidth, lineColor : this.options.scaleLineColor, gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, showLabels : this.options.scaleShowLabels, display : this.options.showScale }; if (this.options.scaleOverride){ helpers.extend(scaleOptions, { calculateYRange: helpers.noop, steps: this.options.scaleSteps, stepValue: this.options.scaleStepWidth, min: this.options.scaleStartValue, max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) }); } this.scale = new Chart.Scale(scaleOptions); }, addData : function(valuesArray,label){ //Map the values array for each of the datasets helpers.each(valuesArray,function(value,datasetIndex){ //Add a new point for each piece of data, passing any required data to draw. this.datasets[datasetIndex].points.push(new this.PointClass({ value : value, label : label, x: this.scale.calculateX(this.scale.valuesCount+1), y: this.scale.endPoint, strokeColor : this.datasets[datasetIndex].pointStrokeColor, fillColor : this.datasets[datasetIndex].pointColor })); },this); this.scale.addXLabel(label); //Then re-render the chart. this.update(); }, removeData : function(){ this.scale.removeXLabel(); //Then re-render the chart. helpers.each(this.datasets,function(dataset){ dataset.points.shift(); },this); this.update(); }, reflow : function(){ var newScaleProps = helpers.extend({ height : this.chart.height, width : this.chart.width }); this.scale.update(newScaleProps); }, draw : function(ease){ var easingDecimal = ease || 1; this.clear(); var ctx = this.chart.ctx; // Some helper methods for getting the next/prev points var hasValue = function(item){ return item.value !== null; }, nextPoint = function(point, collection, index){ return helpers.findNextWhere(collection, hasValue, index) || point; }, previousPoint = function(point, collection, index){ return helpers.findPreviousWhere(collection, hasValue, index) || point; }; this.scale.draw(easingDecimal); helpers.each(this.datasets,function(dataset){ var pointsWithValues = helpers.where(dataset.points, hasValue); //Transition each point first so that the line and point drawing isn't out of sync //We can use this extra loop to calculate the control points of this dataset also in this loop helpers.each(dataset.points, function(point, index){ if (point.hasValue()){ point.transition({ y : this.scale.calculateY(point.value), x : this.scale.calculateX(index) }, easingDecimal); } },this); // Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed if (this.options.bezierCurve){ helpers.each(pointsWithValues, function(point, index){ var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; point.controlPoints = helpers.splineCurve( previousPoint(point, pointsWithValues, index), point, nextPoint(point, pointsWithValues, index), tension ); // Prevent the bezier going outside of the bounds of the graph // Cap puter bezier handles to the upper/lower scale bounds if (point.controlPoints.outer.y > this.scale.endPoint){ point.controlPoints.outer.y = this.scale.endPoint; } else if (point.controlPoints.outer.y < this.scale.startPoint){ point.controlPoints.outer.y = this.scale.startPoint; } // Cap inner bezier handles to the upper/lower scale bounds if (point.controlPoints.inner.y > this.scale.endPoint){ point.controlPoints.inner.y = this.scale.endPoint; } else if (point.controlPoints.inner.y < this.scale.startPoint){ point.controlPoints.inner.y = this.scale.startPoint; } },this); } //Draw the line between all the points ctx.lineWidth = this.options.datasetStrokeWidth; ctx.strokeStyle = dataset.strokeColor; ctx.beginPath(); helpers.each(pointsWithValues, function(point, index){ if (index === 0){ ctx.moveTo(point.x, point.y); } else{ if(this.options.bezierCurve){ var previous = previousPoint(point, pointsWithValues, index); ctx.bezierCurveTo( previous.controlPoints.outer.x, previous.controlPoints.outer.y, point.controlPoints.inner.x, point.controlPoints.inner.y, point.x, point.y ); } else{ ctx.lineTo(point.x,point.y); } } }, this); ctx.stroke(); if (this.options.datasetFill && pointsWithValues.length > 0){ //Round off the line by going to the base of the chart, back to the start, then fill. ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint); ctx.fillStyle = dataset.fillColor; ctx.closePath(); ctx.fill(); } //Now draw the points over the line //A little inefficient double looping, but better than the line //lagging behind the point positions helpers.each(pointsWithValues,function(point){ point.draw(); }); },this); } }); }).call(this); chart-js/chart-demo.js 0000644 00000014734 15030376016 0010652 0 ustar 00 $(function(){ /* Bar */ var randomScalingFactor = function(){ return Math.round(Math.random()*100)}; var barChartData = { labels : ["January","February","March","April","May","June","July"], datasets : [ { fillColor : "rgba(220,220,220,0.5)", strokeColor : "rgba(220,220,220,0.8)", highlightFill: "rgba(220,220,220,0.75)", highlightStroke: "rgba(220,220,220,1)", data : [randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor()] }, { fillColor : "rgba(151,187,205,0.5)", strokeColor : "rgba(151,187,205,0.8)", highlightFill : "rgba(151,187,205,0.75)", highlightStroke : "rgba(151,187,205,1)", data : [randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor()] } ] } var ctx = document.getElementById("canvas-1").getContext("2d"); window.myBar = new Chart(ctx).Bar(barChartData, { responsive : true }); /* Doughnut */ var doughnutData = [ { value: 300, color:"#F7464A", highlight: "#FF5A5E", label: "Red" }, { value: 50, color: "#46BFBD", highlight: "#5AD3D1", label: "Green" }, { value: 100, color: "#FDB45C", highlight: "#FFC870", label: "Yellow" }, { value: 40, color: "#949FB1", highlight: "#A8B3C5", label: "Grey" }, { value: 120, color: "#4D5360", highlight: "#616774", label: "Dark Grey" } ]; var ctx = document.getElementById("chart-area").getContext("2d"); window.myDoughnut = new Chart(ctx).Doughnut(doughnutData, {responsive : true}); /* Line */ var randomScalingFactor = function(){ return Math.round(Math.random()*100)}; var lineChartData = { labels : ["January","February","March","April","May","June","July"], datasets : [ { label: "My First dataset", fillColor : "rgba(220,220,220,0.2)", strokeColor : "rgba(220,220,220,1)", pointColor : "rgba(220,220,220,1)", pointStrokeColor : "#fff", pointHighlightFill : "#fff", pointHighlightStroke : "rgba(220,220,220,1)", data : [randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor()] }, { label: "My Second dataset", fillColor : "rgba(151,187,205,0.2)", strokeColor : "rgba(151,187,205,1)", pointColor : "rgba(151,187,205,1)", pointStrokeColor : "#fff", pointHighlightFill : "#fff", pointHighlightStroke : "rgba(151,187,205,1)", data : [randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor()] } ] } var ctx = document.getElementById("canvas-2").getContext("2d"); window.myLine = new Chart(ctx).Line(lineChartData, { responsive: true }); /* Pie chart */ var pieData = [ { value: 300, color:"#F7464A", highlight: "#FF5A5E", label: "Red" }, { value: 50, color: "#46BFBD", highlight: "#5AD3D1", label: "Green" }, { value: 100, color: "#FDB45C", highlight: "#FFC870", label: "Yellow" }, { value: 40, color: "#949FB1", highlight: "#A8B3C5", label: "Grey" }, { value: 120, color: "#4D5360", highlight: "#616774", label: "Dark Grey" } ]; var ctx = document.getElementById("chart-area-2").getContext("2d"); window.myPie = new Chart(ctx).Pie(pieData, { responsive: true }); /* Polar */ var polarData = [ { value: 300, color:"#F7464A", highlight: "#FF5A5E", label: "Red" }, { value: 50, color: "#46BFBD", highlight: "#5AD3D1", label: "Green" }, { value: 100, color: "#FDB45C", highlight: "#FFC870", label: "Yellow" }, { value: 40, color: "#949FB1", highlight: "#A8B3C5", label: "Grey" }, { value: 120, color: "#4D5360", highlight: "#616774", label: "Dark Grey" } ]; var ctx = document.getElementById("chart-area-3").getContext("2d"); window.myPolarArea = new Chart(ctx).PolarArea(polarData, { responsive:true }); /* Radar chart */ var radarChartData = { labels: ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"], datasets: [ { label: "My First dataset", fillColor: "rgba(220,220,220,0.2)", strokeColor: "rgba(220,220,220,1)", pointColor: "rgba(220,220,220,1)", pointStrokeColor: "#fff", pointHighlightFill: "#fff", pointHighlightStroke: "rgba(220,220,220,1)", data: [65,59,90,81,56,55,40] }, { label: "My Second dataset", fillColor: "rgba(151,187,205,0.2)", strokeColor: "rgba(151,187,205,1)", pointColor: "rgba(151,187,205,1)", pointStrokeColor: "#fff", pointHighlightFill: "#fff", pointHighlightStroke: "rgba(151,187,205,1)", data: [28,48,40,19,96,27,100] } ] }; window.myRadar = new Chart(document.getElementById("canvas-4").getContext("2d")).Radar(radarChartData, { responsive: true }); }); chart-js/chart-core.js 0000644 00000172303 15030376016 0010653 0 ustar 00 /*! * Chart.js * http://chartjs.org/ * Version: {{ version }} * * Copyright 2014 Nick Downie * Released under the MIT license * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md */ (function(){ "use strict"; //Declare root variable - window in the browser, global on the server var root = this, previous = root.Chart; //Occupy the global variable of Chart, and create a simple base class var Chart = function(context){ var chart = this; this.canvas = context.canvas; this.ctx = context; //Variables global to the chart var width = this.width = context.canvas.width; var height = this.height = context.canvas.height; this.aspectRatio = this.width / this.height; //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. helpers.retinaScale(this); return this; }; //Globally expose the defaults to allow for user updating/changing Chart.defaults = { global: { // Boolean - Whether to animate the chart animation: true, // Number - Number of animation steps animationSteps: 60, // String - Animation easing effect animationEasing: "easeOutQuart", // Boolean - If we should show the scale at all showScale: true, // Boolean - If we want to override with a hard coded scale scaleOverride: false, // ** Required if scaleOverride is true ** // Number - The number of steps in a hard coded scale scaleSteps: null, // Number - The value jump in the hard coded scale scaleStepWidth: null, // Number - The scale starting value scaleStartValue: null, // String - Colour of the scale line scaleLineColor: "rgba(0,0,0,.1)", // Number - Pixel width of the scale line scaleLineWidth: 1, // Boolean - Whether to show labels on the scale scaleShowLabels: true, // Interpolated JS string - can access value scaleLabel: "<%=value%>", // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there scaleIntegersOnly: true, // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value scaleBeginAtZero: false, // String - Scale label font declaration for the scale label scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", // Number - Scale label font size in pixels scaleFontSize: 12, // String - Scale label font weight style scaleFontStyle: "normal", // String - Scale label font colour scaleFontColor: "#666", // Boolean - whether or not the chart should be responsive and resize when the browser does. responsive: false, // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container maintainAspectRatio: true, // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove showTooltips: true, // Array - Array of string names to attach tooltip events tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], // String - Tooltip background colour tooltipFillColor: "rgba(0,0,0,0.8)", // String - Tooltip label font declaration for the scale label tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", // Number - Tooltip label font size in pixels tooltipFontSize: 14, // String - Tooltip font weight style tooltipFontStyle: "normal", // String - Tooltip label font colour tooltipFontColor: "#fff", // String - Tooltip title font declaration for the scale label tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", // Number - Tooltip title font size in pixels tooltipTitleFontSize: 14, // String - Tooltip title font weight style tooltipTitleFontStyle: "bold", // String - Tooltip title font colour tooltipTitleFontColor: "#fff", // Number - pixel width of padding around tooltip text tooltipYPadding: 6, // Number - pixel width of padding around tooltip text tooltipXPadding: 6, // Number - Size of the caret on the tooltip tooltipCaretSize: 8, // Number - Pixel radius of the tooltip border tooltipCornerRadius: 6, // Number - Pixel offset from point x to tooltip edge tooltipXOffset: 10, // String - Template string for single tooltips tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", // String - Template string for single tooltips multiTooltipTemplate: "<%= value %>", // String - Colour behind the legend colour block multiTooltipKeyBackground: '#fff', // Function - Will fire on animation progression. onAnimationProgress: function(){}, // Function - Will fire on animation completion. onAnimationComplete: function(){} } }; //Create a dictionary of chart types, to allow for extension of existing types Chart.types = {}; //Global Chart helpers object for utility methods and classes var helpers = Chart.helpers = {}; //-- Basic js utility methods var each = helpers.each = function(loopable,callback,self){ var additionalArgs = Array.prototype.slice.call(arguments, 3); // Check to see if null or undefined firstly. if (loopable){ if (loopable.length === +loopable.length){ var i; for (i=0; i<loopable.length; i++){ callback.apply(self,[loopable[i], i].concat(additionalArgs)); } } else{ for (var item in loopable){ callback.apply(self,[loopable[item],item].concat(additionalArgs)); } } } }, clone = helpers.clone = function(obj){ var objClone = {}; each(obj,function(value,key){ if (obj.hasOwnProperty(key)) objClone[key] = value; }); return objClone; }, extend = helpers.extend = function(base){ each(Array.prototype.slice.call(arguments,1), function(extensionObject) { each(extensionObject,function(value,key){ if (extensionObject.hasOwnProperty(key)) base[key] = value; }); }); return base; }, merge = helpers.merge = function(base,master){ //Merge properties in left object over to a shallow clone of object right. var args = Array.prototype.slice.call(arguments,0); args.unshift({}); return extend.apply(null, args); }, indexOf = helpers.indexOf = function(arrayToSearch, item){ if (Array.prototype.indexOf) { return arrayToSearch.indexOf(item); } else{ for (var i = 0; i < arrayToSearch.length; i++) { if (arrayToSearch[i] === item) return i; } return -1; } }, where = helpers.where = function(collection, filterCallback){ var filtered = []; helpers.each(collection, function(item){ if (filterCallback(item)){ filtered.push(item); } }); return filtered; }, findNextWhere = helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex){ // Default to start of the array if (!startIndex){ startIndex = -1; } for (var i = startIndex + 1; i < arrayToSearch.length; i++) { var currentItem = arrayToSearch[i]; if (filterCallback(currentItem)){ return currentItem; } }; }, findPreviousWhere = helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex){ // Default to end of the array if (!startIndex){ startIndex = arrayToSearch.length; } for (var i = startIndex - 1; i >= 0; i--) { var currentItem = arrayToSearch[i]; if (filterCallback(currentItem)){ return currentItem; } }; }, inherits = helpers.inherits = function(extensions){ //Basic javascript inheritance based on the model created in Backbone.js var parent = this; var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; var Surrogate = function(){ this.constructor = ChartElement;}; Surrogate.prototype = parent.prototype; ChartElement.prototype = new Surrogate(); ChartElement.extend = inherits; if (extensions) extend(ChartElement.prototype, extensions); ChartElement.__super__ = parent.prototype; return ChartElement; }, noop = helpers.noop = function(){}, uid = helpers.uid = (function(){ var id=0; return function(){ return "chart-" + id++; }; })(), warn = helpers.warn = function(str){ //Method for warning of errors if (window.console && typeof window.console.warn == "function") console.warn(str); }, amd = helpers.amd = (typeof define == 'function' && define.amd), //-- Math methods isNumber = helpers.isNumber = function(n){ return !isNaN(parseFloat(n)) && isFinite(n); }, max = helpers.max = function(array){ return Math.max.apply( Math, array ); }, min = helpers.min = function(array){ return Math.min.apply( Math, array ); }, cap = helpers.cap = function(valueToCap,maxValue,minValue){ if(isNumber(maxValue)) { if( valueToCap > maxValue ) { return maxValue; } } else if(isNumber(minValue)){ if ( valueToCap < minValue ){ return minValue; } } return valueToCap; }, getDecimalPlaces = helpers.getDecimalPlaces = function(num){ if (num%1!==0 && isNumber(num)){ return num.toString().split(".")[1].length; } else { return 0; } }, toRadians = helpers.radians = function(degrees){ return degrees * (Math.PI/180); }, // Gets the angle from vertical upright to the point about a centre. getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ var distanceFromXCenter = anglePoint.x - centrePoint.x, distanceFromYCenter = anglePoint.y - centrePoint.y, radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); //If the segment is in the top left quadrant, we need to add another rotation to the angle if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ angle += Math.PI*2; } return { angle: angle, distance: radialDistanceFromCenter }; }, aliasPixel = helpers.aliasPixel = function(pixelWidth){ return (pixelWidth % 2 === 0) ? 0 : 0.5; }, splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ //Props to Rob Spencer at scaled innovation for his post on splining between points //http://scaledinnovation.com/analytics/splines/aboutSplines.html var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), fa=t*d01/(d01+d12),// scaling factor for triangle Ta fb=t*d12/(d01+d12); return { inner : { x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) }, outer : { x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) } }; }, calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ return Math.floor(Math.log(val) / Math.LN10); }, calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ //Set a minimum step of two - a point at the top of the graph, and a point at the base var minSteps = 2, maxSteps = Math.floor(drawingSize/(textSize * 1.5)), skipFitting = (minSteps >= maxSteps); var maxValue = max(valuesArray), minValue = min(valuesArray); // We need some degree of seperation here to calculate the scales if all the values are the same // Adding/minusing 0.5 will give us a range of 1. if (maxValue === minValue){ maxValue += 0.5; // So we don't end up with a graph with a negative start value if we've said always start from zero if (minValue >= 0.5 && !startFromZero){ minValue -= 0.5; } else{ // Make up a whole number above the values maxValue += 0.5; } } var valueRange = Math.abs(maxValue - minValue), rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), graphRange = graphMax - graphMin, stepValue = Math.pow(10, rangeOrderOfMagnitude), numberOfSteps = Math.round(graphRange / stepValue); //If we have more space on the graph we'll use it to give more definition to the data while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { if(numberOfSteps > maxSteps){ stepValue *=2; numberOfSteps = Math.round(graphRange/stepValue); // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. if (numberOfSteps % 1 !== 0){ skipFitting = true; } } //We can fit in double the amount of scale points on the scale else{ //If user has declared ints only, and the step value isn't a decimal if (integersOnly && rangeOrderOfMagnitude >= 0){ //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float if(stepValue/2 % 1 === 0){ stepValue /=2; numberOfSteps = Math.round(graphRange/stepValue); } //If it would make it a float break out of the loop else{ break; } } //If the scale doesn't have to be an int, make the scale more granular anyway. else{ stepValue /=2; numberOfSteps = Math.round(graphRange/stepValue); } } } if (skipFitting){ numberOfSteps = minSteps; stepValue = graphRange / numberOfSteps; } return { steps : numberOfSteps, stepValue : stepValue, min : graphMin, max : graphMin + (numberOfSteps * stepValue) }; }, /* jshint ignore:start */ // Blows up jshint errors based on the new Function constructor //Templating methods //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ template = helpers.template = function(templateString, valuesObject){ // If templateString is function rather than string-template - call the function for valuesObject if(templateString instanceof Function){ return templateString(valuesObject); } var cache = {}; function tmpl(str, data){ // Figure out if we're getting a template, or if we need to // load the template - and be sure to cache the result. var fn = !/\W/.test(str) ? cache[str] = cache[str] : // Generate a reusable function that will serve as a template // generator (and which will be cached). new Function("obj", "var p=[],print=function(){p.push.apply(p,arguments);};" + // Introduce the data as local variables using with(){} "with(obj){p.push('" + // Convert the template into pure JavaScript str .replace(/[\r\t\n]/g, " ") .split("<%").join("\t") .replace(/((^|%>)[^\t]*)'/g, "$1\r") .replace(/\t=(.*?)%>/g, "',$1,'") .split("\t").join("');") .split("%>").join("p.push('") .split("\r").join("\\'") + "');}return p.join('');" ); // Provide some basic currying to the user return data ? fn( data ) : fn; } return tmpl(templateString,valuesObject); }, /* jshint ignore:end */ generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ var labelsArray = new Array(numberOfSteps); if (labelTemplateString){ each(labelsArray,function(val,index){ labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); }); } return labelsArray; }, //--Animation methods //Easing functions adapted from Robert Penner's easing equations //http://www.robertpenner.com/easing/ easingEffects = helpers.easingEffects = { linear: function (t) { return t; }, easeInQuad: function (t) { return t * t; }, easeOutQuad: function (t) { return -1 * t * (t - 2); }, easeInOutQuad: function (t) { if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; return -1 / 2 * ((--t) * (t - 2) - 1); }, easeInCubic: function (t) { return t * t * t; }, easeOutCubic: function (t) { return 1 * ((t = t / 1 - 1) * t * t + 1); }, easeInOutCubic: function (t) { if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t; return 1 / 2 * ((t -= 2) * t * t + 2); }, easeInQuart: function (t) { return t * t * t * t; }, easeOutQuart: function (t) { return -1 * ((t = t / 1 - 1) * t * t * t - 1); }, easeInOutQuart: function (t) { if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t; return -1 / 2 * ((t -= 2) * t * t * t - 2); }, easeInQuint: function (t) { return 1 * (t /= 1) * t * t * t * t; }, easeOutQuint: function (t) { return 1 * ((t = t / 1 - 1) * t * t * t * t + 1); }, easeInOutQuint: function (t) { if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t; return 1 / 2 * ((t -= 2) * t * t * t * t + 2); }, easeInSine: function (t) { return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; }, easeOutSine: function (t) { return 1 * Math.sin(t / 1 * (Math.PI / 2)); }, easeInOutSine: function (t) { return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); }, easeInExpo: function (t) { return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); }, easeOutExpo: function (t) { return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); }, easeInOutExpo: function (t) { if (t === 0) return 0; if (t === 1) return 1; if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1)); return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); }, easeInCirc: function (t) { if (t >= 1) return t; return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); }, easeOutCirc: function (t) { return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); }, easeInOutCirc: function (t) { if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1); return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); }, easeInElastic: function (t) { var s = 1.70158; var p = 0; var a = 1; if (t === 0) return 0; if ((t /= 1) == 1) return 1; if (!p) p = 1 * 0.3; if (a < Math.abs(1)) { a = 1; s = p / 4; } else s = p / (2 * Math.PI) * Math.asin(1 / a); return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); }, easeOutElastic: function (t) { var s = 1.70158; var p = 0; var a = 1; if (t === 0) return 0; if ((t /= 1) == 1) return 1; if (!p) p = 1 * 0.3; if (a < Math.abs(1)) { a = 1; s = p / 4; } else s = p / (2 * Math.PI) * Math.asin(1 / a); return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; }, easeInOutElastic: function (t) { var s = 1.70158; var p = 0; var a = 1; if (t === 0) return 0; if ((t /= 1 / 2) == 2) return 1; if (!p) p = 1 * (0.3 * 1.5); if (a < Math.abs(1)) { a = 1; s = p / 4; } else s = p / (2 * Math.PI) * Math.asin(1 / a); if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; }, easeInBack: function (t) { var s = 1.70158; return 1 * (t /= 1) * t * ((s + 1) * t - s); }, easeOutBack: function (t) { var s = 1.70158; return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); }, easeInOutBack: function (t) { var s = 1.70158; if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); }, easeInBounce: function (t) { return 1 - easingEffects.easeOutBounce(1 - t); }, easeOutBounce: function (t) { if ((t /= 1) < (1 / 2.75)) { return 1 * (7.5625 * t * t); } else if (t < (2 / 2.75)) { return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); } else if (t < (2.5 / 2.75)) { return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); } else { return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); } }, easeInOutBounce: function (t) { if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5; return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; } }, //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ requestAnimFrame = helpers.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) { return window.setTimeout(callback, 1000 / 60); }; })(), cancelAnimFrame = helpers.cancelAnimFrame = (function(){ return window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.oCancelAnimationFrame || window.msCancelAnimationFrame || function(callback) { return window.clearTimeout(callback, 1000 / 60); }; })(), animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ var currentStep = 0, easingFunction = easingEffects[easingString] || easingEffects.linear; var animationFrame = function(){ currentStep++; var stepDecimal = currentStep/totalSteps; var easeDecimal = easingFunction(stepDecimal); callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); onProgress.call(chartInstance,easeDecimal,stepDecimal); if (currentStep < totalSteps){ chartInstance.animationFrame = requestAnimFrame(animationFrame); } else{ onComplete.apply(chartInstance); } }; requestAnimFrame(animationFrame); }, //-- DOM methods getRelativePosition = helpers.getRelativePosition = function(evt){ var mouseX, mouseY; var e = evt.originalEvent || evt, canvas = evt.currentTarget || evt.srcElement, boundingRect = canvas.getBoundingClientRect(); if (e.touches){ mouseX = e.touches[0].clientX - boundingRect.left; mouseY = e.touches[0].clientY - boundingRect.top; } else{ mouseX = e.clientX - boundingRect.left; mouseY = e.clientY - boundingRect.top; } return { x : mouseX, y : mouseY }; }, addEvent = helpers.addEvent = function(node,eventType,method){ if (node.addEventListener){ node.addEventListener(eventType,method); } else if (node.attachEvent){ node.attachEvent("on"+eventType, method); } else { node["on"+eventType] = method; } }, removeEvent = helpers.removeEvent = function(node, eventType, handler){ if (node.removeEventListener){ node.removeEventListener(eventType, handler, false); } else if (node.detachEvent){ node.detachEvent("on"+eventType,handler); } else{ node["on" + eventType] = noop; } }, bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ // Create the events object if it's not already present if (!chartInstance.events) chartInstance.events = {}; each(arrayOfEvents,function(eventName){ chartInstance.events[eventName] = function(){ handler.apply(chartInstance, arguments); }; addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); }); }, unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { each(arrayOfEvents, function(handler,eventName){ removeEvent(chartInstance.chart.canvas, eventName, handler); }); }, getMaximumWidth = helpers.getMaximumWidth = function(domNode){ var container = domNode.parentNode; // TODO = check cross browser stuff with this. return container.clientWidth; }, getMaximumHeight = helpers.getMaximumHeight = function(domNode){ var container = domNode.parentNode; // TODO = check cross browser stuff with this. return container.clientHeight; }, getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support retinaScale = helpers.retinaScale = function(chart){ var ctx = chart.ctx, width = chart.canvas.width, height = chart.canvas.height; if (window.devicePixelRatio) { ctx.canvas.style.width = width + "px"; ctx.canvas.style.height = height + "px"; ctx.canvas.height = height * window.devicePixelRatio; ctx.canvas.width = width * window.devicePixelRatio; ctx.scale(window.devicePixelRatio, window.devicePixelRatio); } }, //-- Canvas methods clear = helpers.clear = function(chart){ chart.ctx.clearRect(0,0,chart.width,chart.height); }, fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ return fontStyle + " " + pixelSize+"px " + fontFamily; }, longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ ctx.font = font; var longest = 0; each(arrayOfStrings,function(string){ var textWidth = ctx.measureText(string).width; longest = (textWidth > longest) ? textWidth : longest; }); return longest; }, drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); }; //Store a reference to each instance - allowing us to globally resize chart instances on window resize. //Destroy method on the chart will remove the instance of the chart from this reference. Chart.instances = {}; Chart.Type = function(data,options,chart){ this.options = options; this.chart = chart; this.id = uid(); //Add the chart instance to the global namespace Chart.instances[this.id] = this; // Initialize is always called when a chart type is created // By default it is a no op, but it should be extended if (options.responsive){ this.resize(); } this.initialize.call(this,data); }; //Core methods that'll be a part of every chart type extend(Chart.Type.prototype,{ initialize : function(){return this;}, clear : function(){ clear(this.chart); return this; }, stop : function(){ // Stops any current animation loop occuring helpers.cancelAnimFrame.call(root, this.animationFrame); return this; }, resize : function(callback){ this.stop(); var canvas = this.chart.canvas, newWidth = getMaximumWidth(this.chart.canvas), newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); canvas.width = this.chart.width = newWidth; canvas.height = this.chart.height = newHeight; retinaScale(this.chart); if (typeof callback === "function"){ callback.apply(this, Array.prototype.slice.call(arguments, 1)); } return this; }, reflow : noop, render : function(reflow){ if (reflow){ this.reflow(); } if (this.options.animation && !reflow){ helpers.animationLoop( this.draw, this.options.animationSteps, this.options.animationEasing, this.options.onAnimationProgress, this.options.onAnimationComplete, this ); } else{ this.draw(); this.options.onAnimationComplete.call(this); } return this; }, generateLegend : function(){ return template(this.options.legendTemplate,this); }, destroy : function(){ this.clear(); unbindEvents(this, this.events); delete Chart.instances[this.id]; }, showTooltip : function(ChartElements, forceRedraw){ // Only redraw the chart if we've actually changed what we're hovering on. if (typeof this.activeElements === 'undefined') this.activeElements = []; var isChanged = (function(Elements){ var changed = false; if (Elements.length !== this.activeElements.length){ changed = true; return changed; } each(Elements, function(element, index){ if (element !== this.activeElements[index]){ changed = true; } }, this); return changed; }).call(this, ChartElements); if (!isChanged && !forceRedraw){ return; } else{ this.activeElements = ChartElements; } this.draw(); if (ChartElements.length > 0){ // If we have multiple datasets, show a MultiTooltip for all of the data points at that index if (this.datasets && this.datasets.length > 1) { var dataArray, dataIndex; for (var i = this.datasets.length - 1; i >= 0; i--) { dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; dataIndex = indexOf(dataArray, ChartElements[0]); if (dataIndex !== -1){ break; } } var tooltipLabels = [], tooltipColors = [], medianPosition = (function(index) { // Get all the points at that particular index var Elements = [], dataCollection, xPositions = [], yPositions = [], xMax, yMax, xMin, yMin; helpers.each(this.datasets, function(dataset){ dataCollection = dataset.points || dataset.bars || dataset.segments; if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ Elements.push(dataCollection[dataIndex]); } }); helpers.each(Elements, function(element) { xPositions.push(element.x); yPositions.push(element.y); //Include any colour information about the element tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); tooltipColors.push({ fill: element._saved.fillColor || element.fillColor, stroke: element._saved.strokeColor || element.strokeColor }); }, this); yMin = min(yPositions); yMax = max(yPositions); xMin = min(xPositions); xMax = max(xPositions); return { x: (xMin > this.chart.width/2) ? xMin : xMax, y: (yMin + yMax)/2 }; }).call(this, dataIndex); new Chart.MultiTooltip({ x: medianPosition.x, y: medianPosition.y, xPadding: this.options.tooltipXPadding, yPadding: this.options.tooltipYPadding, xOffset: this.options.tooltipXOffset, fillColor: this.options.tooltipFillColor, textColor: this.options.tooltipFontColor, fontFamily: this.options.tooltipFontFamily, fontStyle: this.options.tooltipFontStyle, fontSize: this.options.tooltipFontSize, titleTextColor: this.options.tooltipTitleFontColor, titleFontFamily: this.options.tooltipTitleFontFamily, titleFontStyle: this.options.tooltipTitleFontStyle, titleFontSize: this.options.tooltipTitleFontSize, cornerRadius: this.options.tooltipCornerRadius, labels: tooltipLabels, legendColors: tooltipColors, legendColorBackground : this.options.multiTooltipKeyBackground, title: ChartElements[0].label, chart: this.chart, ctx: this.chart.ctx }).draw(); } else { each(ChartElements, function(Element) { var tooltipPosition = Element.tooltipPosition(); new Chart.Tooltip({ x: Math.round(tooltipPosition.x), y: Math.round(tooltipPosition.y), xPadding: this.options.tooltipXPadding, yPadding: this.options.tooltipYPadding, fillColor: this.options.tooltipFillColor, textColor: this.options.tooltipFontColor, fontFamily: this.options.tooltipFontFamily, fontStyle: this.options.tooltipFontStyle, fontSize: this.options.tooltipFontSize, caretHeight: this.options.tooltipCaretSize, cornerRadius: this.options.tooltipCornerRadius, text: template(this.options.tooltipTemplate, Element), chart: this.chart }).draw(); }, this); } } return this; }, toBase64Image : function(){ return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); } }); Chart.Type.extend = function(extensions){ var parent = this; var ChartType = function(){ return parent.apply(this,arguments); }; //Copy the prototype object of the this class ChartType.prototype = clone(parent.prototype); //Now overwrite some of the properties in the base class with the new extensions extend(ChartType.prototype, extensions); ChartType.extend = Chart.Type.extend; if (extensions.name || parent.prototype.name){ var chartName = extensions.name || parent.prototype.name; //Assign any potential default values of the new chart type //If none are defined, we'll use a clone of the chart type this is being extended from. //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart //doesn't define some defaults of their own. var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); Chart.types[chartName] = ChartType; //Register this new chart type in the Chart prototype Chart.prototype[chartName] = function(data,options){ var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); return new ChartType(data,config,this); }; } else{ warn("Name not provided for this chart, so it hasn't been registered"); } return parent; }; Chart.Element = function(configuration){ extend(this,configuration); this.initialize.apply(this,arguments); this.save(); }; extend(Chart.Element.prototype,{ initialize : function(){}, restore : function(props){ if (!props){ extend(this,this._saved); } else { each(props,function(key){ this[key] = this._saved[key]; },this); } return this; }, save : function(){ this._saved = clone(this); delete this._saved._saved; return this; }, update : function(newProps){ each(newProps,function(value,key){ this._saved[key] = this[key]; this[key] = value; },this); return this; }, transition : function(props,ease){ each(props,function(value,key){ this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; },this); return this; }, tooltipPosition : function(){ return { x : this.x, y : this.y }; }, hasValue: function(){ return isNumber(this.value); } }); Chart.Element.extend = inherits; Chart.Point = Chart.Element.extend({ display: true, inRange: function(chartX,chartY){ var hitDetectionRange = this.hitDetectionRadius + this.radius; return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); }, draw : function(){ if (this.display){ var ctx = this.ctx; ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); ctx.closePath(); ctx.strokeStyle = this.strokeColor; ctx.lineWidth = this.strokeWidth; ctx.fillStyle = this.fillColor; ctx.fill(); ctx.stroke(); } //Quick debug for bezier curve splining //Highlights control points and the line between them. //Handy for dev - stripped in the min version. // ctx.save(); // ctx.fillStyle = "black"; // ctx.strokeStyle = "black" // ctx.beginPath(); // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); // ctx.fill(); // ctx.beginPath(); // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); // ctx.fill(); // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); // ctx.lineTo(this.x, this.y); // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); // ctx.stroke(); // ctx.restore(); } }); Chart.Arc = Chart.Element.extend({ inRange : function(chartX,chartY){ var pointRelativePosition = helpers.getAngleFromPoint(this, { x: chartX, y: chartY }); //Check if within the range of the open/close angle var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); return (betweenAngles && withinRadius); //Ensure within the outside of the arc centre, but inside arc outer }, tooltipPosition : function(){ var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; return { x : this.x + (Math.cos(centreAngle) * rangeFromCentre), y : this.y + (Math.sin(centreAngle) * rangeFromCentre) }; }, draw : function(animationPercent){ var easingDecimal = animationPercent || 1; var ctx = this.ctx; ctx.beginPath(); ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); ctx.closePath(); ctx.strokeStyle = this.strokeColor; ctx.lineWidth = this.strokeWidth; ctx.fillStyle = this.fillColor; ctx.fill(); ctx.lineJoin = 'bevel'; if (this.showStroke){ ctx.stroke(); } } }); Chart.Rectangle = Chart.Element.extend({ draw : function(){ var ctx = this.ctx, halfWidth = this.width/2, leftX = this.x - halfWidth, rightX = this.x + halfWidth, top = this.base - (this.base - this.y), halfStroke = this.strokeWidth / 2; // Canvas doesn't allow us to stroke inside the width so we can // adjust the sizes to fit if we're setting a stroke on the line if (this.showStroke){ leftX += halfStroke; rightX -= halfStroke; top += halfStroke; } ctx.beginPath(); ctx.fillStyle = this.fillColor; ctx.strokeStyle = this.strokeColor; ctx.lineWidth = this.strokeWidth; // It'd be nice to keep this class totally generic to any rectangle // and simply specify which border to miss out. ctx.moveTo(leftX, this.base); ctx.lineTo(leftX, top); ctx.lineTo(rightX, top); ctx.lineTo(rightX, this.base); ctx.fill(); if (this.showStroke){ ctx.stroke(); } }, height : function(){ return this.base - this.y; }, inRange : function(chartX,chartY){ return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); } }); Chart.Tooltip = Chart.Element.extend({ draw : function(){ var ctx = this.chart.ctx; ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); this.xAlign = "center"; this.yAlign = "above"; //Distance between the actual element.y position and the start of the tooltip caret var caretPadding = 2; var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, tooltipRectHeight = this.fontSize + 2*this.yPadding, tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; if (this.x + tooltipWidth/2 >this.chart.width){ this.xAlign = "left"; } else if (this.x - tooltipWidth/2 < 0){ this.xAlign = "right"; } if (this.y - tooltipHeight < 0){ this.yAlign = "below"; } var tooltipX = this.x - tooltipWidth/2, tooltipY = this.y - tooltipHeight; ctx.fillStyle = this.fillColor; switch(this.yAlign) { case "above": //Draw a caret above the x/y ctx.beginPath(); ctx.moveTo(this.x,this.y - caretPadding); ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); ctx.closePath(); ctx.fill(); break; case "below": tooltipY = this.y + caretPadding + this.caretHeight; //Draw a caret below the x/y ctx.beginPath(); ctx.moveTo(this.x, this.y + caretPadding); ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); ctx.closePath(); ctx.fill(); break; } switch(this.xAlign) { case "left": tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); break; case "right": tooltipX = this.x - (this.cornerRadius + this.caretHeight); break; } drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); ctx.fill(); ctx.fillStyle = this.textColor; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); } }); Chart.MultiTooltip = Chart.Element.extend({ initialize : function(){ this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; this.ctx.font = this.titleFont; var titleWidth = this.ctx.measureText(this.title).width, //Label has a legend square as well so account for this. labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, longestTextWidth = max([labelWidth,titleWidth]); this.width = longestTextWidth + (this.xPadding*2); var halfHeight = this.height/2; //Check to ensure the height will fit on the canvas //The three is to buffer form the very if (this.y - halfHeight < 0 ){ this.y = halfHeight; } else if (this.y + halfHeight > this.chart.height){ this.y = this.chart.height - halfHeight; } //Decide whether to align left or right based on position on canvas if (this.x > this.chart.width/2){ this.x -= this.xOffset + this.width; } else { this.x += this.xOffset; } }, getLineHeight : function(index){ var baseLineHeight = this.y - (this.height/2) + this.yPadding, afterTitleIndex = index-1; //If the index is zero, we're getting the title if (index === 0){ return baseLineHeight + this.titleFontSize/2; } else{ return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; } }, draw : function(){ drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); var ctx = this.ctx; ctx.fillStyle = this.fillColor; ctx.fill(); ctx.closePath(); ctx.textAlign = "left"; ctx.textBaseline = "middle"; ctx.fillStyle = this.titleTextColor; ctx.font = this.titleFont; ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); ctx.font = this.font; helpers.each(this.labels,function(label,index){ ctx.fillStyle = this.textColor; ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); //Instead we'll make a white filled block to put the legendColour palette over. ctx.fillStyle = this.legendColorBackground; ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); ctx.fillStyle = this.legendColors[index].fill; ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); },this); } }); Chart.Scale = Chart.Element.extend({ initialize : function(){ this.fit(); }, buildYLabels : function(){ this.yLabels = []; var stepDecimalPlaces = getDecimalPlaces(this.stepValue); for (var i=0; i<=this.steps; i++){ this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); } this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0; }, addXLabel : function(label){ this.xLabels.push(label); this.valuesCount++; this.fit(); }, removeXLabel : function(){ this.xLabels.shift(); this.valuesCount--; this.fit(); }, // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use fit: function(){ // First we need the width of the yLabels, assuming the xLabels aren't rotated // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation this.startPoint = (this.display) ? this.fontSize : 0; this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels // Apply padding settings to the start and end point. this.startPoint += this.padding; this.endPoint -= this.padding; // Cache the starting height, so can determine if we need to recalculate the scale yAxis var cachedHeight = this.endPoint - this.startPoint, cachedYLabelWidth; // Build the current yLabels so we have an idea of what size they'll be to start /* * This sets what is returned from calculateScaleRange as static properties of this class: * this.steps; this.stepValue; this.min; this.max; * */ this.calculateYRange(cachedHeight); // With these properties set we can now build the array of yLabels // and also the width of the largest yLabel this.buildYLabels(); this.calculateXLabelRotation(); while((cachedHeight > this.endPoint - this.startPoint)){ cachedHeight = this.endPoint - this.startPoint; cachedYLabelWidth = this.yLabelWidth; this.calculateYRange(cachedHeight); this.buildYLabels(); // Only go through the xLabel loop again if the yLabel width has changed if (cachedYLabelWidth < this.yLabelWidth){ this.calculateXLabelRotation(); } } }, calculateXLabelRotation : function(){ //Get the width of each grid by calculating the difference //between x offsets between 0 and 1. this.ctx.font = this.font; var firstWidth = this.ctx.measureText(this.xLabels[0]).width, lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, firstRotated, lastRotated; this.xScalePaddingRight = lastWidth/2 + 3; this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; this.xLabelRotation = 0; if (this.display){ var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), cosRotation, firstRotatedWidth; this.xLabelWidth = originalLabelWidth; //Allow 3 pixels x2 padding either side for label readability var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; //Max label rotate should be 90 - also act as a loop counter while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ cosRotation = Math.cos(toRadians(this.xLabelRotation)); firstRotated = cosRotation * firstWidth; lastRotated = cosRotation * lastWidth; // We're right aligning the text now. if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ this.xScalePaddingLeft = firstRotated + this.fontSize / 2; } this.xScalePaddingRight = this.fontSize/2; this.xLabelRotation++; this.xLabelWidth = cosRotation * originalLabelWidth; } if (this.xLabelRotation > 0){ this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; } } else{ this.xLabelWidth = 0; this.xScalePaddingRight = this.padding; this.xScalePaddingLeft = this.padding; } }, // Needs to be overidden in each Chart type // Otherwise we need to pass all the data into the scale class calculateYRange: noop, drawingArea: function(){ return this.startPoint - this.endPoint; }, calculateY : function(value){ var scalingFactor = this.drawingArea() / (this.min - this.max); return this.endPoint - (scalingFactor * (value - this.min)); }, calculateX : function(index){ var isRotated = (this.xLabelRotation > 0), // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), valueWidth = innerWidth/(this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), valueOffset = (valueWidth * index) + this.xScalePaddingLeft; if (this.offsetGridLines){ valueOffset += (valueWidth/2); } return Math.round(valueOffset); }, update : function(newProps){ helpers.extend(this, newProps); this.fit(); }, draw : function(){ var ctx = this.ctx, yLabelGap = (this.endPoint - this.startPoint) / this.steps, xStart = Math.round(this.xScalePaddingLeft); if (this.display){ ctx.fillStyle = this.textColor; ctx.font = this.font; each(this.yLabels,function(labelString,index){ var yLabelCenter = this.endPoint - (yLabelGap * index), linePositionY = Math.round(yLabelCenter); ctx.textAlign = "right"; ctx.textBaseline = "middle"; if (this.showLabels){ ctx.fillText(labelString,xStart - 10,yLabelCenter); } ctx.beginPath(); if (index > 0){ // This is a grid line in the centre, so drop that ctx.lineWidth = this.gridLineWidth; ctx.strokeStyle = this.gridLineColor; } else { // This is the first line on the scale ctx.lineWidth = this.lineWidth; ctx.strokeStyle = this.lineColor; } linePositionY += helpers.aliasPixel(ctx.lineWidth); ctx.moveTo(xStart, linePositionY); ctx.lineTo(this.width, linePositionY); ctx.stroke(); ctx.closePath(); ctx.lineWidth = this.lineWidth; ctx.strokeStyle = this.lineColor; ctx.beginPath(); ctx.moveTo(xStart - 5, linePositionY); ctx.lineTo(xStart, linePositionY); ctx.stroke(); ctx.closePath(); },this); each(this.xLabels,function(label,index){ var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), // Check to see if line/bar here and decide where to place the line linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), isRotated = (this.xLabelRotation > 0); ctx.beginPath(); if (index > 0){ // This is a grid line in the centre, so drop that ctx.lineWidth = this.gridLineWidth; ctx.strokeStyle = this.gridLineColor; } else { // This is the first line on the scale ctx.lineWidth = this.lineWidth; ctx.strokeStyle = this.lineColor; } ctx.moveTo(linePos,this.endPoint); ctx.lineTo(linePos,this.startPoint - 3); ctx.stroke(); ctx.closePath(); ctx.lineWidth = this.lineWidth; ctx.strokeStyle = this.lineColor; // Small lines at the bottom of the base grid line ctx.beginPath(); ctx.moveTo(linePos,this.endPoint); ctx.lineTo(linePos,this.endPoint + 5); ctx.stroke(); ctx.closePath(); ctx.save(); ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); ctx.rotate(toRadians(this.xLabelRotation)*-1); ctx.font = this.font; ctx.textAlign = (isRotated) ? "right" : "center"; ctx.textBaseline = (isRotated) ? "middle" : "top"; ctx.fillText(label, 0, 0); ctx.restore(); },this); } } }); Chart.RadialScale = Chart.Element.extend({ initialize: function(){ this.size = min([this.height, this.width]); this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); }, calculateCenterOffset: function(value){ // Take into account half font size + the yPadding of the top value var scalingFactor = this.drawingArea / (this.max - this.min); return (value - this.min) * scalingFactor; }, update : function(){ if (!this.lineArc){ this.setScaleSize(); } else { this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); } this.buildYLabels(); }, buildYLabels: function(){ this.yLabels = []; var stepDecimalPlaces = getDecimalPlaces(this.stepValue); for (var i=0; i<=this.steps; i++){ this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); } }, getCircumference : function(){ return ((Math.PI*2) / this.valuesCount); }, setScaleSize: function(){ /* * Right, this is really confusing and there is a lot of maths going on here * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 * * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif * * Solution: * * We assume the radius of the polygon is half the size of the canvas at first * at each index we check if the text overlaps. * * Where it does, we store that angle and that index. * * After finding the largest index and angle we calculate how much we need to remove * from the shape radius to move the point inwards by that x. * * We average the left and right distances to get the maximum shape radius that can fit in the box * along with labels. * * Once we have that, we can find the centre point for the chart, by taking the x text protrusion * on each side, removing that from the size, halving it and adding the left x protrusion width. * * This will mean we have a shape fitted to the canvas, as large as it can be with the labels * and position it in the most space efficient manner * * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif */ // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), pointPosition, i, textWidth, halfTextWidth, furthestRight = this.width, furthestRightIndex, furthestRightAngle, furthestLeft = 0, furthestLeftIndex, furthestLeftAngle, xProtrusionLeft, xProtrusionRight, radiusReductionRight, radiusReductionLeft, maxWidthRadius; this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); for (i=0;i<this.valuesCount;i++){ // 5px to space the text slightly out - similar to what we do in the draw function. pointPosition = this.getPointPosition(i, largestPossibleRadius); textWidth = this.ctx.measureText(template(this.templateString, { value: this.labels[i] })).width + 5; if (i === 0 || i === this.valuesCount/2){ // If we're at index zero, or exactly the middle, we're at exactly the top/bottom // of the radar chart, so text will be aligned centrally, so we'll half it and compare // w/left and right text sizes halfTextWidth = textWidth/2; if (pointPosition.x + halfTextWidth > furthestRight) { furthestRight = pointPosition.x + halfTextWidth; furthestRightIndex = i; } if (pointPosition.x - halfTextWidth < furthestLeft) { furthestLeft = pointPosition.x - halfTextWidth; furthestLeftIndex = i; } } else if (i < this.valuesCount/2) { // Less than half the values means we'll left align the text if (pointPosition.x + textWidth > furthestRight) { furthestRight = pointPosition.x + textWidth; furthestRightIndex = i; } } else if (i > this.valuesCount/2){ // More than half the values means we'll right align the text if (pointPosition.x - textWidth < furthestLeft) { furthestLeft = pointPosition.x - textWidth; furthestLeftIndex = i; } } } xProtrusionLeft = furthestLeft; xProtrusionRight = Math.ceil(furthestRight - this.width); furthestRightAngle = this.getIndexAngle(furthestRightIndex); furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); // Ensure we actually need to reduce the size of the chart radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) this.setCenterPoint(radiusReductionLeft, radiusReductionRight); }, setCenterPoint: function(leftMovement, rightMovement){ var maxRight = this.width - rightMovement - this.drawingArea, maxLeft = leftMovement + this.drawingArea; this.xCenter = (maxLeft + maxRight)/2; // Always vertically in the centre as the text height doesn't change this.yCenter = (this.height/2); }, getIndexAngle : function(index){ var angleMultiplier = (Math.PI * 2) / this.valuesCount; // Start from the top instead of right, so remove a quarter of the circle return index * angleMultiplier - (Math.PI/2); }, getPointPosition : function(index, distanceFromCenter){ var thisAngle = this.getIndexAngle(index); return { x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter }; }, draw: function(){ if (this.display){ var ctx = this.ctx; each(this.yLabels, function(label, index){ // Don't draw a centre value if (index > 0){ var yCenterOffset = index * (this.drawingArea/this.steps), yHeight = this.yCenter - yCenterOffset, pointPosition; // Draw circular lines around the scale if (this.lineWidth > 0){ ctx.strokeStyle = this.lineColor; ctx.lineWidth = this.lineWidth; if(this.lineArc){ ctx.beginPath(); ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); ctx.closePath(); ctx.stroke(); } else{ ctx.beginPath(); for (var i=0;i<this.valuesCount;i++) { pointPosition = this.getPointPosition(i, this.calculateCenterOffset(this.min + (index * this.stepValue))); if (i === 0){ ctx.moveTo(pointPosition.x, pointPosition.y); } else { ctx.lineTo(pointPosition.x, pointPosition.y); } } ctx.closePath(); ctx.stroke(); } } if(this.showLabels){ ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); if (this.showLabelBackdrop){ var labelWidth = ctx.measureText(label).width; ctx.fillStyle = this.backdropColor; ctx.fillRect( this.xCenter - labelWidth/2 - this.backdropPaddingX, yHeight - this.fontSize/2 - this.backdropPaddingY, labelWidth + this.backdropPaddingX*2, this.fontSize + this.backdropPaddingY*2 ); } ctx.textAlign = 'center'; ctx.textBaseline = "middle"; ctx.fillStyle = this.fontColor; ctx.fillText(label, this.xCenter, yHeight); } } }, this); if (!this.lineArc){ ctx.lineWidth = this.angleLineWidth; ctx.strokeStyle = this.angleLineColor; for (var i = this.valuesCount - 1; i >= 0; i--) { if (this.angleLineWidth > 0){ var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); ctx.beginPath(); ctx.moveTo(this.xCenter, this.yCenter); ctx.lineTo(outerPosition.x, outerPosition.y); ctx.stroke(); ctx.closePath(); } // Extra 3px out for some label spacing var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); ctx.fillStyle = this.pointLabelFontColor; var labelsCount = this.labels.length, halfLabelsCount = this.labels.length/2, quarterLabelsCount = halfLabelsCount/2, upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); if (i === 0){ ctx.textAlign = 'center'; } else if(i === halfLabelsCount){ ctx.textAlign = 'center'; } else if (i < halfLabelsCount){ ctx.textAlign = 'left'; } else { ctx.textAlign = 'right'; } // Set the correct text baseline based on outer positioning if (exactQuarter){ ctx.textBaseline = 'middle'; } else if (upperHalf){ ctx.textBaseline = 'bottom'; } else { ctx.textBaseline = 'top'; } ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); } } } } }); // Attach global event to resize each chart instance when the browser resizes helpers.addEvent(window, "resize", (function(){ // Basic debounce of resize function so it doesn't hurt performance when resizing browser. var timeout; return function(){ clearTimeout(timeout); timeout = setTimeout(function(){ each(Chart.instances,function(instance){ // If the responsive flag is set in the chart instance config // Cascade the resize event down to the chart. if (instance.options.responsive){ instance.resize(instance.render, true); } }); }, 50); }; })()); if (amd) { define(function(){ return Chart; }); } else if (typeof module === 'object' && module.exports) { module.exports = Chart; } root.Chart = Chart; Chart.noConflict = function(){ root.Chart = previous; return Chart; }; }).call(this); chart-js/chart-polar.js 0000644 00000016703 15030376016 0011041 0 ustar 00 (function(){ "use strict"; var root = this, Chart = root.Chart, //Cache a local reference to Chart.helpers helpers = Chart.helpers; var defaultConfig = { //Boolean - Show a backdrop to the scale label scaleShowLabelBackdrop : true, //String - The colour of the label backdrop scaleBackdropColor : "rgba(255,255,255,0.75)", // Boolean - Whether the scale should begin at zero scaleBeginAtZero : true, //Number - The backdrop padding above & below the label in pixels scaleBackdropPaddingY : 2, //Number - The backdrop padding to the side of the label in pixels scaleBackdropPaddingX : 2, //Boolean - Show line for each value in the scale scaleShowLine : true, //Boolean - Stroke a line around each segment in the chart segmentShowStroke : true, //String - The colour of the stroke on each segement. segmentStrokeColor : "#fff", //Number - The width of the stroke value in pixels segmentStrokeWidth : 2, //Number - Amount of animation steps animationSteps : 100, //String - Animation easing effect. animationEasing : "easeOutBounce", //Boolean - Whether to animate the rotation of the chart animateRotate : true, //Boolean - Whether to animate scaling the chart from the centre animateScale : false, //String - A legend template legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>" }; Chart.Type.extend({ //Passing in a name registers this chart in the Chart namespace name: "PolarArea", //Providing a defaults will also register the deafults in the chart namespace defaults : defaultConfig, //Initialize is fired when the chart is initialized - Data is passed in as a parameter //Config is automatically merged by the core of Chart.js, and is available at this.options initialize: function(data){ this.segments = []; //Declare segment class as a chart instance specific class, so it can share props for this instance this.SegmentArc = Chart.Arc.extend({ showStroke : this.options.segmentShowStroke, strokeWidth : this.options.segmentStrokeWidth, strokeColor : this.options.segmentStrokeColor, ctx : this.chart.ctx, innerRadius : 0, x : this.chart.width/2, y : this.chart.height/2 }); this.scale = new Chart.RadialScale({ display: this.options.showScale, fontStyle: this.options.scaleFontStyle, fontSize: this.options.scaleFontSize, fontFamily: this.options.scaleFontFamily, fontColor: this.options.scaleFontColor, showLabels: this.options.scaleShowLabels, showLabelBackdrop: this.options.scaleShowLabelBackdrop, backdropColor: this.options.scaleBackdropColor, backdropPaddingY : this.options.scaleBackdropPaddingY, backdropPaddingX: this.options.scaleBackdropPaddingX, lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, lineColor: this.options.scaleLineColor, lineArc: true, width: this.chart.width, height: this.chart.height, xCenter: this.chart.width/2, yCenter: this.chart.height/2, ctx : this.chart.ctx, templateString: this.options.scaleLabel, valuesCount: data.length }); this.updateScaleRange(data); this.scale.update(); helpers.each(data,function(segment,index){ this.addData(segment,index,true); },this); //Set up tooltip events on the chart if (this.options.showTooltips){ helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; helpers.each(this.segments,function(segment){ segment.restore(["fillColor"]); }); helpers.each(activeSegments,function(activeSegment){ activeSegment.fillColor = activeSegment.highlightColor; }); this.showTooltip(activeSegments); }); } this.render(); }, getSegmentsAtEvent : function(e){ var segmentsArray = []; var location = helpers.getRelativePosition(e); helpers.each(this.segments,function(segment){ if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); },this); return segmentsArray; }, addData : function(segment, atIndex, silent){ var index = atIndex || this.segments.length; this.segments.splice(index, 0, new this.SegmentArc({ fillColor: segment.color, highlightColor: segment.highlight || segment.color, label: segment.label, value: segment.value, outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), startAngle: Math.PI * 1.5 })); if (!silent){ this.reflow(); this.update(); } }, removeData: function(atIndex){ var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; this.segments.splice(indexToDelete, 1); this.reflow(); this.update(); }, calculateTotal: function(data){ this.total = 0; helpers.each(data,function(segment){ this.total += segment.value; },this); this.scale.valuesCount = this.segments.length; }, updateScaleRange: function(datapoints){ var valuesArray = []; helpers.each(datapoints,function(segment){ valuesArray.push(segment.value); }); var scaleSizes = (this.options.scaleOverride) ? { steps: this.options.scaleSteps, stepValue: this.options.scaleStepWidth, min: this.options.scaleStartValue, max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) } : helpers.calculateScaleRange( valuesArray, helpers.min([this.chart.width, this.chart.height])/2, this.options.scaleFontSize, this.options.scaleBeginAtZero, this.options.scaleIntegersOnly ); helpers.extend( this.scale, scaleSizes, { size: helpers.min([this.chart.width, this.chart.height]), xCenter: this.chart.width/2, yCenter: this.chart.height/2 } ); }, update : function(){ this.calculateTotal(this.segments); helpers.each(this.segments,function(segment){ segment.save(); }); this.render(); }, reflow : function(){ helpers.extend(this.SegmentArc.prototype,{ x : this.chart.width/2, y : this.chart.height/2 }); this.updateScaleRange(this.segments); this.scale.update(); helpers.extend(this.scale,{ xCenter: this.chart.width/2, yCenter: this.chart.height/2 }); helpers.each(this.segments, function(segment){ segment.update({ outerRadius : this.scale.calculateCenterOffset(segment.value) }); }, this); }, draw : function(ease){ var easingDecimal = ease || 1; //Clear & draw the canvas this.clear(); helpers.each(this.segments,function(segment, index){ segment.transition({ circumference : this.scale.getCircumference(), outerRadius : this.scale.calculateCenterOffset(segment.value) },easingDecimal); segment.endAngle = segment.startAngle + segment.circumference; // If we've removed the first segment we need to set the first one to // start at the top. if (index === 0){ segment.startAngle = Math.PI * 1.5; } //Check to see if it's the last segment, if not get the next and update the start angle if (index < this.segments.length - 1){ this.segments[index+1].startAngle = segment.endAngle; } segment.draw(); }, this); this.scale.draw(); } }); }).call(this); chart-js/chart-doughnut.js 0000644 00000013271 15030376016 0011556 0 ustar 00 (function(){ "use strict"; var root = this, Chart = root.Chart, //Cache a local reference to Chart.helpers helpers = Chart.helpers; var defaultConfig = { //Boolean - Whether we should show a stroke on each segment segmentShowStroke : true, //String - The colour of each segment stroke segmentStrokeColor : "#fff", //Number - The width of each segment stroke segmentStrokeWidth : 2, //The percentage of the chart that we cut out of the middle. percentageInnerCutout : 50, //Number - Amount of animation steps animationSteps : 100, //String - Animation easing effect animationEasing : "easeOutBounce", //Boolean - Whether we animate the rotation of the Doughnut animateRotate : true, //Boolean - Whether we animate scaling the Doughnut from the centre animateScale : false, //String - A legend template legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>" }; Chart.Type.extend({ //Passing in a name registers this chart in the Chart namespace name: "Doughnut", //Providing a defaults will also register the deafults in the chart namespace defaults : defaultConfig, //Initialize is fired when the chart is initialized - Data is passed in as a parameter //Config is automatically merged by the core of Chart.js, and is available at this.options initialize: function(data){ //Declare segments as a static property to prevent inheriting across the Chart type prototype this.segments = []; this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; this.SegmentArc = Chart.Arc.extend({ ctx : this.chart.ctx, x : this.chart.width/2, y : this.chart.height/2 }); //Set up tooltip events on the chart if (this.options.showTooltips){ helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; helpers.each(this.segments,function(segment){ segment.restore(["fillColor"]); }); helpers.each(activeSegments,function(activeSegment){ activeSegment.fillColor = activeSegment.highlightColor; }); this.showTooltip(activeSegments); }); } this.calculateTotal(data); helpers.each(data,function(datapoint, index){ this.addData(datapoint, index, true); },this); this.render(); }, getSegmentsAtEvent : function(e){ var segmentsArray = []; var location = helpers.getRelativePosition(e); helpers.each(this.segments,function(segment){ if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); },this); return segmentsArray; }, addData : function(segment, atIndex, silent){ var index = atIndex || this.segments.length; this.segments.splice(index, 0, new this.SegmentArc({ value : segment.value, outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, fillColor : segment.color, highlightColor : segment.highlight || segment.color, showStroke : this.options.segmentShowStroke, strokeWidth : this.options.segmentStrokeWidth, strokeColor : this.options.segmentStrokeColor, startAngle : Math.PI * 1.5, circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), label : segment.label })); if (!silent){ this.reflow(); this.update(); } }, calculateCircumference : function(value){ return (Math.PI*2)*(value / this.total); }, calculateTotal : function(data){ this.total = 0; helpers.each(data,function(segment){ this.total += segment.value; },this); }, update : function(){ this.calculateTotal(this.segments); // Reset any highlight colours before updating. helpers.each(this.activeElements, function(activeElement){ activeElement.restore(['fillColor']); }); helpers.each(this.segments,function(segment){ segment.save(); }); this.render(); }, removeData: function(atIndex){ var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; this.segments.splice(indexToDelete, 1); this.reflow(); this.update(); }, reflow : function(){ helpers.extend(this.SegmentArc.prototype,{ x : this.chart.width/2, y : this.chart.height/2 }); this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; helpers.each(this.segments, function(segment){ segment.update({ outerRadius : this.outerRadius, innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout }); }, this); }, draw : function(easeDecimal){ var animDecimal = (easeDecimal) ? easeDecimal : 1; this.clear(); helpers.each(this.segments,function(segment,index){ segment.transition({ circumference : this.calculateCircumference(segment.value), outerRadius : this.outerRadius, innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout },animDecimal); segment.endAngle = segment.startAngle + segment.circumference; segment.draw(); if (index === 0){ segment.startAngle = Math.PI * 1.5; } //Check to see if it's the last segment, if not get the next and update the start angle if (index < this.segments.length-1){ this.segments[index+1].startAngle = segment.endAngle; } },this); } }); Chart.types.Doughnut.extend({ name : "Pie", defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) }); }).call(this); flot/flot-tooltip.js 0000644 00000056346 15030376016 0010521 0 ustar 00 /* * jquery.flot.tooltip * * description: easy-to-use tooltips for Flot charts * version: 0.9.0 * authors: Krzysztof Urbas @krzysu [myviews.pl],Evan Steinkerchner @Roundaround * website: https://github.com/krzysu/flot.tooltip * * build on 2016-07-26 * released under MIT License, 2012 */ (function ($) { // plugin options, default values var defaultOptions = { tooltip: { show: false, cssClass: "flotTip", content: "%s | X: %x | Y: %y", // allowed templates are: // %s -> series label, // %c -> series color, // %lx -> x axis label (requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels), // %ly -> y axis label (requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels), // %x -> X value, // %y -> Y value, // %x.2 -> precision of X value, // %p -> percent // %n -> value (not percent) of pie chart xDateFormat: null, yDateFormat: null, monthNames: null, dayNames: null, shifts: { x: 10, y: 20 }, defaultTheme: true, snap: true, lines: false, clickTips: false, // callbacks onHover: function (flotItem, $tooltipEl) {}, $compat: false } }; // dummy default options object for legacy code (<0.8.5) - is deleted later defaultOptions.tooltipOpts = defaultOptions.tooltip; // object var FlotTooltip = function (plot) { // variables this.tipPosition = {x: 0, y: 0}; this.init(plot); }; // main plugin function FlotTooltip.prototype.init = function (plot) { var that = this; // detect other flot plugins var plotPluginsLength = $.plot.plugins.length; this.plotPlugins = []; if (plotPluginsLength) { for (var p = 0; p < plotPluginsLength; p++) { this.plotPlugins.push($.plot.plugins[p].name); } } plot.hooks.bindEvents.push(function (plot, eventHolder) { // get plot options that.plotOptions = plot.getOptions(); // for legacy (<0.8.5) implementations if (typeof(that.plotOptions.tooltip) === 'boolean') { that.plotOptions.tooltipOpts.show = that.plotOptions.tooltip; that.plotOptions.tooltip = that.plotOptions.tooltipOpts; delete that.plotOptions.tooltipOpts; } // if not enabled return if (that.plotOptions.tooltip.show === false || typeof that.plotOptions.tooltip.show === 'undefined') return; // shortcut to access tooltip options that.tooltipOptions = that.plotOptions.tooltip; if (that.tooltipOptions.$compat) { that.wfunc = 'width'; that.hfunc = 'height'; } else { that.wfunc = 'innerWidth'; that.hfunc = 'innerHeight'; } // create tooltip DOM element var $tip = that.getDomElement(); // bind event $( plot.getPlaceholder() ).bind("plothover", plothover); if (that.tooltipOptions.clickTips) { $( plot.getPlaceholder() ).bind("plotclick", plotclick); } that.clickmode = false; $(eventHolder).bind('mousemove', mouseMove); }); plot.hooks.shutdown.push(function (plot, eventHolder){ $(plot.getPlaceholder()).unbind("plothover", plothover); $(plot.getPlaceholder()).unbind("plotclick", plotclick); plot.removeTooltip(); $(eventHolder).unbind("mousemove", mouseMove); }); function mouseMove(e){ var pos = {}; pos.x = e.pageX; pos.y = e.pageY; plot.setTooltipPosition(pos); } /** * open the tooltip (if not already open) and freeze it on the current position till the next click */ function plotclick(event, pos, item) { if (! that.clickmode) { // it is the click activating the clicktip plothover(event, pos, item); if (that.getDomElement().is(":visible")) { $(plot.getPlaceholder()).unbind("plothover", plothover); that.clickmode = true; } } else { // it is the click deactivating the clicktip $( plot.getPlaceholder() ).bind("plothover", plothover); plot.hideTooltip(); that.clickmode = false; } } function plothover(event, pos, item) { // Simple distance formula. var lineDistance = function (p1x, p1y, p2x, p2y) { return Math.sqrt((p2x - p1x) * (p2x - p1x) + (p2y - p1y) * (p2y - p1y)); }; // Here is some voodoo magic for determining the distance to a line form a given point {x, y}. var dotLineLength = function (x, y, x0, y0, x1, y1, o) { if (o && !(o = function (x, y, x0, y0, x1, y1) { if (typeof x0 !== 'undefined') return { x: x0, y: y }; else if (typeof y0 !== 'undefined') return { x: x, y: y0 }; var left, tg = -1 / ((y1 - y0) / (x1 - x0)); return { x: left = (x1 * (x * tg - y + y0) + x0 * (x * -tg + y - y1)) / (tg * (x1 - x0) + y0 - y1), y: tg * left - tg * x + y }; } (x, y, x0, y0, x1, y1), o.x >= Math.min(x0, x1) && o.x <= Math.max(x0, x1) && o.y >= Math.min(y0, y1) && o.y <= Math.max(y0, y1)) ) { var l1 = lineDistance(x, y, x0, y0), l2 = lineDistance(x, y, x1, y1); return l1 > l2 ? l2 : l1; } else { var a = y0 - y1, b = x1 - x0, c = x0 * y1 - y0 * x1; return Math.abs(a * x + b * y + c) / Math.sqrt(a * a + b * b); } }; if (item) { plot.showTooltip(item, that.tooltipOptions.snap ? item : pos); } else if (that.plotOptions.series.lines.show && that.tooltipOptions.lines === true) { var maxDistance = that.plotOptions.grid.mouseActiveRadius; var closestTrace = { distance: maxDistance + 1 }; var ttPos = pos; $.each(plot.getData(), function (i, series) { var xBeforeIndex = 0, xAfterIndex = -1; // Our search here assumes our data is sorted via the x-axis. // TODO: Improve efficiency somehow - search smaller sets of data. for (var j = 1; j < series.data.length; j++) { if (series.data[j - 1][0] <= pos.x && series.data[j][0] >= pos.x) { xBeforeIndex = j - 1; xAfterIndex = j; } } if (xAfterIndex === -1) { plot.hideTooltip(); return; } var pointPrev = { x: series.data[xBeforeIndex][0], y: series.data[xBeforeIndex][1] }, pointNext = { x: series.data[xAfterIndex][0], y: series.data[xAfterIndex][1] }; var distToLine = dotLineLength(series.xaxis.p2c(pos.x), series.yaxis.p2c(pos.y), series.xaxis.p2c(pointPrev.x), series.yaxis.p2c(pointPrev.y), series.xaxis.p2c(pointNext.x), series.yaxis.p2c(pointNext.y), false); if (distToLine < closestTrace.distance) { var closestIndex = lineDistance(pointPrev.x, pointPrev.y, pos.x, pos.y) < lineDistance(pos.x, pos.y, pointNext.x, pointNext.y) ? xBeforeIndex : xAfterIndex; var pointSize = series.datapoints.pointsize; // Calculate the point on the line vertically closest to our cursor. var pointOnLine = [ pos.x, pointPrev.y + ((pointNext.y - pointPrev.y) * ((pos.x - pointPrev.x) / (pointNext.x - pointPrev.x))) ]; var item = { datapoint: pointOnLine, dataIndex: closestIndex, series: series, seriesIndex: i }; closestTrace = { distance: distToLine, item: item }; if (that.tooltipOptions.snap) { ttPos = { pageX: series.xaxis.p2c(pointOnLine[0]), pageY: series.yaxis.p2c(pointOnLine[1]) }; } } }); if (closestTrace.distance < maxDistance + 1) plot.showTooltip(closestTrace.item, ttPos); else plot.hideTooltip(); } else { plot.hideTooltip(); } } // Quick little function for setting the tooltip position. plot.setTooltipPosition = function (pos) { var $tip = that.getDomElement(); var totalTipWidth = $tip.outerWidth() + that.tooltipOptions.shifts.x; var totalTipHeight = $tip.outerHeight() + that.tooltipOptions.shifts.y; if ((pos.x - $(window).scrollLeft()) > ($(window)[that.wfunc]() - totalTipWidth)) { pos.x -= totalTipWidth; pos.x = Math.max(pos.x, 0); } if ((pos.y - $(window).scrollTop()) > ($(window)[that.hfunc]() - totalTipHeight)) { pos.y -= totalTipHeight; } /* The section applies the new positioning ONLY if pos.x and pos.y are numbers. If they are undefined or not a number, use the last known numerical position. This hack fixes a bug that kept pie charts from keeping their tooltip positioning. */ if (isNaN(pos.x)) { that.tipPosition.x = that.tipPosition.xPrev; } else { that.tipPosition.x = pos.x; that.tipPosition.xPrev = pos.x; } if (isNaN(pos.y)) { that.tipPosition.y = that.tipPosition.yPrev; } else { that.tipPosition.y = pos.y; that.tipPosition.yPrev = pos.y; } }; // Quick little function for showing the tooltip. plot.showTooltip = function (target, position, targetPosition) { var $tip = that.getDomElement(); // convert tooltip content template to real tipText var tipText = that.stringFormat(that.tooltipOptions.content, target); if (tipText === '') return; $tip.html(tipText); plot.setTooltipPosition({ x: that.tipPosition.x, y: that.tipPosition.y }); $tip.css({ left: that.tipPosition.x + that.tooltipOptions.shifts.x, top: that.tipPosition.y + that.tooltipOptions.shifts.y }).show(); // run callback if (typeof that.tooltipOptions.onHover === 'function') { that.tooltipOptions.onHover(target, $tip); } }; // Quick little function for hiding the tooltip. plot.hideTooltip = function () { that.getDomElement().hide().html(''); }; plot.removeTooltip = function() { that.getDomElement().remove(); }; }; /** * get or create tooltip DOM element * @return jQuery object */ FlotTooltip.prototype.getDomElement = function () { var $tip = $('<div>'); if (this.tooltipOptions && this.tooltipOptions.cssClass) { $tip = $('.' + this.tooltipOptions.cssClass); if( $tip.length === 0 ){ $tip = $('<div />').addClass(this.tooltipOptions.cssClass); $tip.appendTo('body').hide().css({position: 'absolute'}); if(this.tooltipOptions.defaultTheme) { $tip.css({ 'background': '#fff', 'z-index': '1040', 'padding': '0.4em 0.6em', 'border-radius': '0.5em', 'font-size': '0.8em', 'border': '1px solid #111', 'display': 'none', 'white-space': 'nowrap' }); } } } return $tip; }; /** * core function, create tooltip content * @param {string} content - template with tooltip content * @param {object} item - Flot item * @return {string} real tooltip content for current item */ FlotTooltip.prototype.stringFormat = function (content, item) { var percentPattern = /%p\.{0,1}(\d{0,})/; var seriesPattern = /%s/; var colorPattern = /%c/; var xLabelPattern = /%lx/; // requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels, will be ignored if plugin isn't loaded var yLabelPattern = /%ly/; // requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels, will be ignored if plugin isn't loaded var xPattern = /%x\.{0,1}(\d{0,})/; var yPattern = /%y\.{0,1}(\d{0,})/; var xPatternWithoutPrecision = "%x"; var yPatternWithoutPrecision = "%y"; var customTextPattern = "%ct"; var nPiePattern = "%n"; var x, y, customText, p, n; // for threshold plugin we need to read data from different place if (typeof item.series.threshold !== "undefined") { x = item.datapoint[0]; y = item.datapoint[1]; customText = item.datapoint[2]; } // for CurvedLines plugin we need to read data from different place else if (typeof item.series.curvedLines !== "undefined") { x = item.datapoint[0]; y = item.datapoint[1]; } else if (typeof item.series.lines !== "undefined" && item.series.lines.steps) { x = item.series.datapoints.points[item.dataIndex * 2]; y = item.series.datapoints.points[item.dataIndex * 2 + 1]; // TODO: where to find custom text in this variant? customText = ""; } else { x = item.series.data[item.dataIndex][0]; y = item.series.data[item.dataIndex][1]; customText = item.series.data[item.dataIndex][2]; } // I think this is only in case of threshold plugin if (item.series.label === null && item.series.originSeries) { item.series.label = item.series.originSeries.label; } // if it is a function callback get the content string if (typeof(content) === 'function') { content = content(item.series.label, x, y, item); } // the case where the passed content is equal to false if (typeof(content) === 'boolean' && !content) { return ''; } /* replacement of %ct and other multi-character templates must precede the replacement of single-character templates to avoid conflict between '%c' and '%ct' and similar substrings */ if (customText) { content = content.replace(customTextPattern, customText); } // percent match for pie charts and stacked percent if (typeof (item.series.percent) !== 'undefined') { p = item.series.percent; } else if (typeof (item.series.percents) !== 'undefined') { p = item.series.percents[item.dataIndex]; } if (typeof p === 'number') { content = this.adjustValPrecision(percentPattern, content, p); } // replace %n with number of items represented by slice in pie charts if (item.series.hasOwnProperty('pie')) { if (typeof item.series.data[0][1] !== 'undefined') { n = item.series.data[0][1]; } } if (typeof n === 'number') { content = content.replace(nPiePattern, n); } // series match if (typeof(item.series.label) !== 'undefined') { content = content.replace(seriesPattern, item.series.label); } else { //remove %s if label is undefined content = content.replace(seriesPattern, ""); } // color match if (typeof(item.series.color) !== 'undefined') { content = content.replace(colorPattern, item.series.color); } else { //remove %s if color is undefined content = content.replace(colorPattern, ""); } // x axis label match if (this.hasAxisLabel('xaxis', item)) { content = content.replace(xLabelPattern, item.series.xaxis.options.axisLabel); } else { //remove %lx if axis label is undefined or axislabels plugin not present content = content.replace(xLabelPattern, ""); } // y axis label match if (this.hasAxisLabel('yaxis', item)) { content = content.replace(yLabelPattern, item.series.yaxis.options.axisLabel); } else { //remove %ly if axis label is undefined or axislabels plugin not present content = content.replace(yLabelPattern, ""); } // time mode axes with custom dateFormat if (this.isTimeMode('xaxis', item) && this.isXDateFormat(item)) { content = content.replace(xPattern, this.timestampToDate(x, this.tooltipOptions.xDateFormat, item.series.xaxis.options)); } if (this.isTimeMode('yaxis', item) && this.isYDateFormat(item)) { content = content.replace(yPattern, this.timestampToDate(y, this.tooltipOptions.yDateFormat, item.series.yaxis.options)); } // set precision if defined if (typeof x === 'number') { content = this.adjustValPrecision(xPattern, content, x); } if (typeof y === 'number') { content = this.adjustValPrecision(yPattern, content, y); } // change x from number to given label, if given if (typeof item.series.xaxis.ticks !== 'undefined') { var ticks; if (this.hasRotatedXAxisTicks(item)) { // xaxis.ticks will be an empty array if tickRotor is being used, but the values are available in rotatedTicks ticks = 'rotatedTicks'; } else { ticks = 'ticks'; } // see https://github.com/krzysu/flot.tooltip/issues/65 var tickIndex = item.dataIndex + item.seriesIndex; for (var xIndex in item.series.xaxis[ticks]) { if (item.series.xaxis[ticks].hasOwnProperty(tickIndex) && !this.isTimeMode('xaxis', item)) { var valueX = (this.isCategoriesMode('xaxis', item)) ? item.series.xaxis[ticks][tickIndex].label : item.series.xaxis[ticks][tickIndex].v; if (valueX === x) { content = content.replace(xPattern, item.series.xaxis[ticks][tickIndex].label.replace(/\$/g, '$$$$')); } } } } // change y from number to given label, if given if (typeof item.series.yaxis.ticks !== 'undefined') { for (var yIndex in item.series.yaxis.ticks) { if (item.series.yaxis.ticks.hasOwnProperty(yIndex)) { var valueY = (this.isCategoriesMode('yaxis', item)) ? item.series.yaxis.ticks[yIndex].label : item.series.yaxis.ticks[yIndex].v; if (valueY === y) { content = content.replace(yPattern, item.series.yaxis.ticks[yIndex].label.replace(/\$/g, '$$$$')); } } } } // if no value customization, use tickFormatter by default if (typeof item.series.xaxis.tickFormatter !== 'undefined') { //escape dollar content = content.replace(xPatternWithoutPrecision, item.series.xaxis.tickFormatter(x, item.series.xaxis).replace(/\$/g, '$$')); } if (typeof item.series.yaxis.tickFormatter !== 'undefined') { //escape dollar content = content.replace(yPatternWithoutPrecision, item.series.yaxis.tickFormatter(y, item.series.yaxis).replace(/\$/g, '$$')); } return content; }; // helpers just for readability FlotTooltip.prototype.isTimeMode = function (axisName, item) { return (typeof item.series[axisName].options.mode !== 'undefined' && item.series[axisName].options.mode === 'time'); }; FlotTooltip.prototype.isXDateFormat = function (item) { return (typeof this.tooltipOptions.xDateFormat !== 'undefined' && this.tooltipOptions.xDateFormat !== null); }; FlotTooltip.prototype.isYDateFormat = function (item) { return (typeof this.tooltipOptions.yDateFormat !== 'undefined' && this.tooltipOptions.yDateFormat !== null); }; FlotTooltip.prototype.isCategoriesMode = function (axisName, item) { return (typeof item.series[axisName].options.mode !== 'undefined' && item.series[axisName].options.mode === 'categories'); }; // FlotTooltip.prototype.timestampToDate = function (tmst, dateFormat, options) { var theDate = $.plot.dateGenerator(tmst, options); return $.plot.formatDate(theDate, dateFormat, this.tooltipOptions.monthNames, this.tooltipOptions.dayNames); }; // FlotTooltip.prototype.adjustValPrecision = function (pattern, content, value) { var precision; var matchResult = content.match(pattern); if( matchResult !== null ) { if(RegExp.$1 !== '') { precision = RegExp.$1; value = value.toFixed(precision); // only replace content if precision exists, in other case use thickformater content = content.replace(pattern, value); } } return content; }; // other plugins detection below // check if flot-axislabels plugin (https://github.com/markrcote/flot-axislabels) is used and that an axis label is given FlotTooltip.prototype.hasAxisLabel = function (axisName, item) { return ($.inArray('axisLabels', this.plotPlugins) !== -1 && typeof item.series[axisName].options.axisLabel !== 'undefined' && item.series[axisName].options.axisLabel.length > 0); }; // check whether flot-tickRotor, a plugin which allows rotation of X-axis ticks, is being used FlotTooltip.prototype.hasRotatedXAxisTicks = function (item) { return ($.inArray('tickRotor',this.plotPlugins) !== -1 && typeof item.series.xaxis.rotatedTicks !== 'undefined'); }; // var init = function (plot) { new FlotTooltip(plot); }; // define Flot plugin $.plot.plugins.push({ init: init, options: defaultOptions, name: 'tooltip', version: '0.8.5' }); })(jQuery); flot/flot-resize.js 0000644 00000006344 15030376016 0010321 0 ustar 00 /* Flot plugin for automatically redrawing plots as the placeholder resizes. Copyright (c) 2007-2014 IOLA and Ole Laursen. Licensed under the MIT license. It works by listening for changes on the placeholder div (through the jQuery resize event plugin) - if the size changes, it will redraw the plot. There are no options. If you need to disable the plugin for some plots, you can just fix the size of their placeholders. */ /* Inline dependency: * jQuery resize event - v1.1 - 3/14/2010 * http://benalman.com/projects/jquery-resize-plugin/ * * Copyright (c) 2010 "Cowboy" Ben Alman * Dual licensed under the MIT and GPL licenses. * http://benalman.com/about/license/ */ (function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this); (function ($) { var options = { }; // no options function init(plot) { function onResize() { var placeholder = plot.getPlaceholder(); // somebody might have hidden us and we can't plot // when we don't have the dimensions if (placeholder.width() == 0 || placeholder.height() == 0) return; plot.resize(); plot.setupGrid(); plot.draw(); } function bindEvents(plot, eventHolder) { plot.getPlaceholder().resize(onResize); } function shutdown(plot, eventHolder) { plot.getPlaceholder().unbind("resize", onResize); } plot.hooks.bindEvents.push(bindEvents); plot.hooks.shutdown.push(shutdown); } $.plot.plugins.push({ init: init, options: options, name: 'resize', version: '1.0' }); })(jQuery); flot/flot-demo.js 0000644 00000016543 15030376016 0007746 0 ustar 00 $(function() { //Interacting with Data Points example var sin = [], cos = []; for (var i = 0; i < 354; i += 31) { sin.push([i, Math.random(i)]); cos.push([i, Math.random(i)]); } var plot = $.plot($("#data-example-1"), [{ data: sin, label: "Today" }, { data: cos, label: "Yesterday" }], { series: { shadowSize: 0, lines: { show: true, lineWidth: 2 }, points: { show: true } }, grid: { labelMargin: 10, hoverable: true, clickable: true, borderWidth: 1, borderColor: 'rgba(82, 167, 224, 0.06)' }, legend: { backgroundColor: '#fff' }, yaxis: { tickColor: 'rgba(0, 0, 0, 0.06)', font: {color: 'rgba(0, 0, 0, 0.4)'}}, xaxis: { tickColor: 'rgba(0, 0, 0, 0.06)', font: {color: 'rgba(0, 0, 0, 0.4)'}}, colors: [getUIColor('success'), getUIColor('gray')], tooltip: true, tooltipOpts: { content: "x: %x, y: %y" } }); var previousPoint = null; $("#data-example-1").bind("plothover", function (event, pos, item) { $("#x").text(pos.x.toFixed(2)); $("#y").text(pos.y.toFixed(2)); }); $("#data-example-1").bind("plotclick", function (event, pos, item) { if (item) { $("#clickdata").text("You clicked point " + item.dataIndex + " in " + item.series.label + "."); plot.highlight(item.series, item.datapoint); } }); }); $(function() { var d1 = []; for (var i = 0; i <= 10; i += 1) { d1.push([i, parseInt(Math.random() * 30)]); } var d2 = []; for (var i = 0; i <= 10; i += 1) { d2.push([i, parseInt(Math.random() * 30)]); } var d3 = []; for (var i = 0; i <= 10; i += 1) { d3.push([i, parseInt(Math.random() * 30)]); } var d4 = []; for (var i = 0; i <= 10; i += 1) { d4.push([i, parseInt(Math.random() * 30)]); } var stack = 0, bars = true, lines = false, steps = false; function plotWithOptions() { $.plot("#data-example-2", [ d1, d2, d3, d4 ], { series: { shadowSize: 0, stack: stack, lines: { show: lines, lineWidth: 1, }, bars: { show: bars, lineWidth: 1, } }, grid: { labelMargin: 10, borderWidth: 0 }, legend: { backgroundColor: '#fff' }, yaxis: { tickColor: 'rgba(0, 0, 0, 0.06)', font: {color: 'rgba(0, 0, 0, 0.4)'}}, xaxis: { tickColor: 'rgba(0, 0, 0, 0.06)', font: {color: 'rgba(0, 0, 0, 0.4)'}}, colors: [getUIColor('default'),getUIColor('warning'),getUIColor('danger'),getUIColor('primary')], tooltip: true, tooltipOpts: { content: "x: %x, y: %y" } }); } plotWithOptions(); $(".stackControls button").click(function (e) { e.preventDefault(); stack = $(this).text() == "With stacking" ? true : null; plotWithOptions(); }); $(".graphControls button").click(function (e) { e.preventDefault(); bars = $(this).text().indexOf("Bars") != -1; lines = $(this).text().indexOf("Lines") != -1; steps = $(this).text().indexOf("steps") != -1; plotWithOptions(); }); }); $(function() { // We use an inline data source in the example, usually data would // be fetched from a server var data = [], totalPoints = 300; function getRandomData() { if (data.length > 0) data = data.slice(1); // Do a random walk while (data.length < totalPoints) { var prev = data.length > 0 ? data[data.length - 1] : 50, y = prev + Math.random() * 10 - 5; if (y < 0) { y = 0; } else if (y > 100) { y = 100; } data.push(y); } // Zip the generated y values with the x values var res = []; for (var i = 0; i < data.length; ++i) { res.push([i, data[i]]) } return res; } // Set up the control widget var updateInterval = 30; var plot = $.plot("#data-example-3", [ getRandomData() ], { series: { lines: { show: true, lineWidth: 2, fill: 0.5, fillColor: { colors: [ { opacity: 0.01 }, { opacity: 0.08 } ] } }, shadowSize: 0 // Drawing is faster without shadows }, grid: { labelMargin: 10, hoverable: true, clickable: true, borderWidth: 1, borderColor: 'rgba(82, 167, 224, 0.06)' }, yaxis: { min: 0, max: 150, tickColor: 'rgba(0, 0, 0, 0.06)', font: {color: 'rgba(0, 0, 0, 0.4)'}}, xaxis: { show: false }, colors: [getUIColor('default'),getUIColor('gray')] }); function update() { plot.setData([getRandomData()]); // Since the axes don't change, we don't need to call plot.setupGrid() plot.draw(); setTimeout(update, updateInterval); } update(); }); $(function() { // Randomly Generated Data var dataSet = [ {label: "Asia", data: 1119630000, color: getUIColor('info') }, { label: "Latin America", data: 690950000, color: getUIColor('warning') }, { label: "Africa", data: 1012960000, color: getUIColor('danger') }, { label: "Oceania", data: 5100000, color: getUIColor('gray') }, { label: "Europe", data: 727080000, color: getUIColor('primary') }, { label: "North America", data: 344120000, color: getUIColor('success') } ]; var data = [], series = Math.floor(Math.random() * 5) + 3; for (var i = 0; i < series; i++) { data[i] = { label: "Series" + (i + 1), data: Math.floor(Math.random() * 100) + 1 } } $.plot('#data-donut-1', dataSet, { series: { pie: { innerRadius: 0.5, show: true, }, } }); $.plot('#data-donut-2', dataSet, { series: { pie: { show: true }, }, tooltip: true, tooltipOpts: { content: "%p.0%, %s" }, grid: { hoverable: true, clickable: true } }); $.plot('#data-donut-3', dataSet, { series: { pie: { show: true, radius: 500, label: { show: true, formatter: labelFormatter, threshold: 0.1 } }, }, legend: { show: false } }); function labelFormatter(label, series) { return "<div style='font-size:12px; text-align:center; padding:5px; color:white;'>" + label + "<br/>" + Math.round(series.percent) + "%</div>"; } }); flot/flot.js 0000644 00000366302 15030376016 0007025 0 ustar 00 /* Javascript plotting library for jQuery, version 0.8.3. Copyright (c) 2007-2014 IOLA and Ole Laursen. Licensed under the MIT license. */ // first an inline dependency, jquery.colorhelpers.js, we inline it here // for convenience /* Plugin for jQuery for working with colors. * * Version 1.1. * * Inspiration from jQuery color animation plugin by John Resig. * * Released under the MIT license by Ole Laursen, October 2009. * * Examples: * * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() * var c = $.color.extract($("#mydiv"), 'background-color'); * console.log(c.r, c.g, c.b, c.a); * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" * * Note that .scale() and .add() return the same modified object * instead of making a new one. * * V. 1.1: Fix error handling so e.g. parsing an empty string does * produce a color rather than just crashing. */ (function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i<c.length;++i)o[c.charAt(i)]+=d;return o.normalize()};o.scale=function(c,f){for(var i=0;i<c.length;++i)o[c.charAt(i)]*=f;return o.normalize()};o.toString=function(){if(o.a>=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return value<min?min:value>max?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); // the actual Flot code (function($) { // Cache the prototype hasOwnProperty for faster access var hasOwnProperty = Object.prototype.hasOwnProperty; // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM // operation produces the same effect as detach, i.e. removing the element // without touching its jQuery data. // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+. if (!$.fn.detach) { $.fn.detach = function() { return this.each(function() { if (this.parentNode) { this.parentNode.removeChild( this ); } }); }; } /////////////////////////////////////////////////////////////////////////// // The Canvas object is a wrapper around an HTML5 <canvas> tag. // // @constructor // @param {string} cls List of classes to apply to the canvas. // @param {element} container Element onto which to append the canvas. // // Requiring a container is a little iffy, but unfortunately canvas // operations don't work unless the canvas is attached to the DOM. function Canvas(cls, container) { var element = container.children("." + cls)[0]; if (element == null) { element = document.createElement("canvas"); element.className = cls; $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) .appendTo(container); // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas if (!element.getContext) { if (window.G_vmlCanvasManager) { element = window.G_vmlCanvasManager.initElement(element); } else { throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); } } } this.element = element; var context = this.context = element.getContext("2d"); // Determine the screen's ratio of physical to device-independent // pixels. This is the ratio between the canvas width that the browser // advertises and the number of pixels actually present in that space. // The iPhone 4, for example, has a device-independent width of 320px, // but its screen is actually 640px wide. It therefore has a pixel // ratio of 2, while most normal devices have a ratio of 1. var devicePixelRatio = window.devicePixelRatio || 1, backingStoreRatio = context.webkitBackingStorePixelRatio || context.mozBackingStorePixelRatio || context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || context.backingStorePixelRatio || 1; this.pixelRatio = devicePixelRatio / backingStoreRatio; // Size the canvas to match the internal dimensions of its container this.resize(container.width(), container.height()); // Collection of HTML div layers for text overlaid onto the canvas this.textContainer = null; this.text = {}; // Cache of text fragments and metrics, so we can avoid expensively // re-calculating them when the plot is re-rendered in a loop. this._textCache = {}; } // Resizes the canvas to the given dimensions. // // @param {number} width New width of the canvas, in pixels. // @param {number} width New height of the canvas, in pixels. Canvas.prototype.resize = function(width, height) { if (width <= 0 || height <= 0) { throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); } var element = this.element, context = this.context, pixelRatio = this.pixelRatio; // Resize the canvas, increasing its density based on the display's // pixel ratio; basically giving it more pixels without increasing the // size of its element, to take advantage of the fact that retina // displays have that many more pixels in the same advertised space. // Resizing should reset the state (excanvas seems to be buggy though) if (this.width != width) { element.width = width * pixelRatio; element.style.width = width + "px"; this.width = width; } if (this.height != height) { element.height = height * pixelRatio; element.style.height = height + "px"; this.height = height; } // Save the context, so we can reset in case we get replotted. The // restore ensure that we're really back at the initial state, and // should be safe even if we haven't saved the initial state yet. context.restore(); context.save(); // Scale the coordinate space to match the display density; so even though we // may have twice as many pixels, we still want lines and other drawing to // appear at the same size; the extra pixels will just make them crisper. context.scale(pixelRatio, pixelRatio); }; // Clears the entire canvas area, not including any overlaid HTML text Canvas.prototype.clear = function() { this.context.clearRect(0, 0, this.width, this.height); }; // Finishes rendering the canvas, including managing the text overlay. Canvas.prototype.render = function() { var cache = this._textCache; // For each text layer, add elements marked as active that haven't // already been rendered, and remove those that are no longer active. for (var layerKey in cache) { if (hasOwnProperty.call(cache, layerKey)) { var layer = this.getTextLayer(layerKey), layerCache = cache[layerKey]; layer.hide(); for (var styleKey in layerCache) { if (hasOwnProperty.call(layerCache, styleKey)) { var styleCache = layerCache[styleKey]; for (var key in styleCache) { if (hasOwnProperty.call(styleCache, key)) { var positions = styleCache[key].positions; for (var i = 0, position; position = positions[i]; i++) { if (position.active) { if (!position.rendered) { layer.append(position.element); position.rendered = true; } } else { positions.splice(i--, 1); if (position.rendered) { position.element.detach(); } } } if (positions.length == 0) { delete styleCache[key]; } } } } } layer.show(); } } }; // Creates (if necessary) and returns the text overlay container. // // @param {string} classes String of space-separated CSS classes used to // uniquely identify the text layer. // @return {object} The jQuery-wrapped text-layer div. Canvas.prototype.getTextLayer = function(classes) { var layer = this.text[classes]; // Create the text layer if it doesn't exist if (layer == null) { // Create the text layer container, if it doesn't exist if (this.textContainer == null) { this.textContainer = $("<div class='flot-text'></div>") .css({ position: "absolute", top: 0, left: 0, bottom: 0, right: 0, 'font-size': "smaller", color: "#545454" }) .insertAfter(this.element); } layer = this.text[classes] = $("<div></div>") .addClass(classes) .css({ position: "absolute", top: 0, left: 0, bottom: 0, right: 0 }) .appendTo(this.textContainer); } return layer; }; // Creates (if necessary) and returns a text info object. // // The object looks like this: // // { // width: Width of the text's wrapper div. // height: Height of the text's wrapper div. // element: The jQuery-wrapped HTML div containing the text. // positions: Array of positions at which this text is drawn. // } // // The positions array contains objects that look like this: // // { // active: Flag indicating whether the text should be visible. // rendered: Flag indicating whether the text is currently visible. // element: The jQuery-wrapped HTML div containing the text. // x: X coordinate at which to draw the text. // y: Y coordinate at which to draw the text. // } // // Each position after the first receives a clone of the original element. // // The idea is that that the width, height, and general 'identity' of the // text is constant no matter where it is placed; the placements are a // secondary property. // // Canvas maintains a cache of recently-used text info objects; getTextInfo // either returns the cached element or creates a new entry. // // @param {string} layer A string of space-separated CSS classes uniquely // identifying the layer containing this text. // @param {string} text Text string to retrieve info for. // @param {(string|object)=} font Either a string of space-separated CSS // classes or a font-spec object, defining the text's font and style. // @param {number=} angle Angle at which to rotate the text, in degrees. // Angle is currently unused, it will be implemented in the future. // @param {number=} width Maximum width of the text before it wraps. // @return {object} a text info object. Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { var textStyle, layerCache, styleCache, info; // Cast the value to a string, in case we were given a number or such text = "" + text; // If the font is a font-spec object, generate a CSS font definition if (typeof font === "object") { textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; } else { textStyle = font; } // Retrieve (or create) the cache for the text's layer and styles layerCache = this._textCache[layer]; if (layerCache == null) { layerCache = this._textCache[layer] = {}; } styleCache = layerCache[textStyle]; if (styleCache == null) { styleCache = layerCache[textStyle] = {}; } info = styleCache[text]; // If we can't find a matching element in our cache, create a new one if (info == null) { var element = $("<div></div>").html(text) .css({ position: "absolute", 'max-width': width, top: -9999 }) .appendTo(this.getTextLayer(layer)); if (typeof font === "object") { element.css({ font: textStyle, color: font.color }); } else if (typeof font === "string") { element.addClass(font); } info = styleCache[text] = { width: element.outerWidth(true), height: element.outerHeight(true), element: element, positions: [] }; element.detach(); } return info; }; // Adds a text string to the canvas text overlay. // // The text isn't drawn immediately; it is marked as rendering, which will // result in its addition to the canvas on the next render pass. // // @param {string} layer A string of space-separated CSS classes uniquely // identifying the layer containing this text. // @param {number} x X coordinate at which to draw the text. // @param {number} y Y coordinate at which to draw the text. // @param {string} text Text string to draw. // @param {(string|object)=} font Either a string of space-separated CSS // classes or a font-spec object, defining the text's font and style. // @param {number=} angle Angle at which to rotate the text, in degrees. // Angle is currently unused, it will be implemented in the future. // @param {number=} width Maximum width of the text before it wraps. // @param {string=} halign Horizontal alignment of the text; either "left", // "center" or "right". // @param {string=} valign Vertical alignment of the text; either "top", // "middle" or "bottom". Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { var info = this.getTextInfo(layer, text, font, angle, width), positions = info.positions; // Tweak the div's position to match the text's alignment if (halign == "center") { x -= info.width / 2; } else if (halign == "right") { x -= info.width; } if (valign == "middle") { y -= info.height / 2; } else if (valign == "bottom") { y -= info.height; } // Determine whether this text already exists at this position. // If so, mark it for inclusion in the next render pass. for (var i = 0, position; position = positions[i]; i++) { if (position.x == x && position.y == y) { position.active = true; return; } } // If the text doesn't exist at this position, create a new entry // For the very first position we'll re-use the original element, // while for subsequent ones we'll clone it. position = { active: true, rendered: false, element: positions.length ? info.element.clone() : info.element, x: x, y: y }; positions.push(position); // Move the element to its final position within the container position.element.css({ top: Math.round(y), left: Math.round(x), 'text-align': halign // In case the text wraps }); }; // Removes one or more text strings from the canvas text overlay. // // If no parameters are given, all text within the layer is removed. // // Note that the text is not immediately removed; it is simply marked as // inactive, which will result in its removal on the next render pass. // This avoids the performance penalty for 'clear and redraw' behavior, // where we potentially get rid of all text on a layer, but will likely // add back most or all of it later, as when redrawing axes, for example. // // @param {string} layer A string of space-separated CSS classes uniquely // identifying the layer containing this text. // @param {number=} x X coordinate of the text. // @param {number=} y Y coordinate of the text. // @param {string=} text Text string to remove. // @param {(string|object)=} font Either a string of space-separated CSS // classes or a font-spec object, defining the text's font and style. // @param {number=} angle Angle at which the text is rotated, in degrees. // Angle is currently unused, it will be implemented in the future. Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { if (text == null) { var layerCache = this._textCache[layer]; if (layerCache != null) { for (var styleKey in layerCache) { if (hasOwnProperty.call(layerCache, styleKey)) { var styleCache = layerCache[styleKey]; for (var key in styleCache) { if (hasOwnProperty.call(styleCache, key)) { var positions = styleCache[key].positions; for (var i = 0, position; position = positions[i]; i++) { position.active = false; } } } } } } } else { var positions = this.getTextInfo(layer, text, font, angle).positions; for (var i = 0, position; position = positions[i]; i++) { if (position.x == x && position.y == y) { position.active = false; } } } }; /////////////////////////////////////////////////////////////////////////// // The top-level container for the entire plot. function Plot(placeholder, data_, options_, plugins) { // data is on the form: // [ series1, series2 ... ] // where series is either just the data as [ [x1, y1], [x2, y2], ... ] // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } var series = [], options = { // the color theme used for graphs colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], legend: { show: true, noColumns: 1, // number of colums in legend table labelFormatter: null, // fn: string -> string labelBoxBorderColor: "#ccc", // border color for the little label boxes container: null, // container (as jQuery object) to put legend in, null means default on top of graph position: "ne", // position of default legend container within plot margin: 5, // distance from grid edge to default legend container within plot backgroundColor: null, // null means auto-detect backgroundOpacity: 0.85, // set to 0 to avoid background sorted: null // default to no legend sorting }, xaxis: { show: null, // null = auto-detect, true = always, false = never position: "bottom", // or "top" mode: null, // null or "time" font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } color: null, // base color, labels, ticks tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" transform: null, // null or f: number -> number to transform axis inverseTransform: null, // if transform is set, this should be the inverse function min: null, // min. value to show, null means set automatically max: null, // max. value to show, null means set automatically autoscaleMargin: null, // margin in % to add if auto-setting min/max ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks tickFormatter: null, // fn: number -> string labelWidth: null, // size of tick labels in pixels labelHeight: null, reserveSpace: null, // whether to reserve space even if axis isn't shown tickLength: null, // size in pixels of ticks, or "full" for whole line alignTicksWithAxis: null, // axis number or null for no sync tickDecimals: null, // no. of decimals, null means auto tickSize: null, // number or [number, "unit"] minTickSize: null // number or [number, "unit"] }, yaxis: { autoscaleMargin: 0.02, position: "left" // or "right" }, xaxes: [], yaxes: [], series: { points: { show: false, radius: 3, lineWidth: 2, // in pixels fill: true, fillColor: "#ffffff", symbol: "circle" // or callback }, lines: { // we don't put in show: false so we can see // whether lines were actively disabled lineWidth: 2, // in pixels fill: false, fillColor: null, steps: false // Omit 'zero', so we can later default its value to // match that of the 'fill' option. }, bars: { show: false, lineWidth: 2, // in pixels barWidth: 1, // in units of the x axis fill: true, fillColor: null, align: "left", // "left", "right", or "center" horizontal: false, zero: true }, shadowSize: 3, highlightColor: null }, grid: { show: true, aboveData: false, color: "#545454", // primary color used for outline and labels backgroundColor: null, // null for transparent, else color borderColor: null, // set if different from the grid color tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" margin: 0, // distance from the canvas edge to the grid labelMargin: 5, // in pixels axisMargin: 8, // in pixels borderWidth: 2, // in pixels minBorderMargin: null, // in pixels, null means taken from points radius markings: null, // array of ranges or fn: axes -> array of ranges markingsColor: "#f4f4f4", markingsLineWidth: 2, // interactive stuff clickable: false, hoverable: false, autoHighlight: true, // highlight in case mouse is near mouseActiveRadius: 10 // how far the mouse can be away to activate an item }, interaction: { redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow }, hooks: {} }, surface = null, // the canvas for the plot itself overlay = null, // canvas for interactive stuff on top of plot eventHolder = null, // jQuery object that events should be bound to ctx = null, octx = null, xaxes = [], yaxes = [], plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, plotWidth = 0, plotHeight = 0, hooks = { processOptions: [], processRawData: [], processDatapoints: [], processOffset: [], drawBackground: [], drawSeries: [], draw: [], bindEvents: [], drawOverlay: [], shutdown: [] }, plot = this; // public functions plot.setData = setData; plot.setupGrid = setupGrid; plot.draw = draw; plot.getPlaceholder = function() { return placeholder; }; plot.getCanvas = function() { return surface.element; }; plot.getPlotOffset = function() { return plotOffset; }; plot.width = function () { return plotWidth; }; plot.height = function () { return plotHeight; }; plot.offset = function () { var o = eventHolder.offset(); o.left += plotOffset.left; o.top += plotOffset.top; return o; }; plot.getData = function () { return series; }; plot.getAxes = function () { var res = {}, i; $.each(xaxes.concat(yaxes), function (_, axis) { if (axis) res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; }); return res; }; plot.getXAxes = function () { return xaxes; }; plot.getYAxes = function () { return yaxes; }; plot.c2p = canvasToAxisCoords; plot.p2c = axisToCanvasCoords; plot.getOptions = function () { return options; }; plot.highlight = highlight; plot.unhighlight = unhighlight; plot.triggerRedrawOverlay = triggerRedrawOverlay; plot.pointOffset = function(point) { return { left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) }; }; plot.shutdown = shutdown; plot.destroy = function () { shutdown(); placeholder.removeData("plot").empty(); series = []; options = null; surface = null; overlay = null; eventHolder = null; ctx = null; octx = null; xaxes = []; yaxes = []; hooks = null; highlights = []; plot = null; }; plot.resize = function () { var width = placeholder.width(), height = placeholder.height(); surface.resize(width, height); overlay.resize(width, height); }; // public attributes plot.hooks = hooks; // initialize initPlugins(plot); parseOptions(options_); setupCanvases(); setData(data_); setupGrid(); draw(); bindEvents(); function executeHooks(hook, args) { args = [plot].concat(args); for (var i = 0; i < hook.length; ++i) hook[i].apply(this, args); } function initPlugins() { // References to key classes, allowing plugins to modify them var classes = { Canvas: Canvas }; for (var i = 0; i < plugins.length; ++i) { var p = plugins[i]; p.init(plot, classes); if (p.options) $.extend(true, options, p.options); } } function parseOptions(opts) { $.extend(true, options, opts); // $.extend merges arrays, rather than replacing them. When less // colors are provided than the size of the default palette, we // end up with those colors plus the remaining defaults, which is // not expected behavior; avoid it by replacing them here. if (opts && opts.colors) { options.colors = opts.colors; } if (options.xaxis.color == null) options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); if (options.yaxis.color == null) options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; if (options.grid.borderColor == null) options.grid.borderColor = options.grid.color; if (options.grid.tickColor == null) options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); // Fill in defaults for axis options, including any unspecified // font-spec fields, if a font-spec was provided. // If no x/y axis options were provided, create one of each anyway, // since the rest of the code assumes that they exist. var i, axisOptions, axisCount, fontSize = placeholder.css("font-size"), fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, fontDefaults = { style: placeholder.css("font-style"), size: Math.round(0.8 * fontSizeDefault), variant: placeholder.css("font-variant"), weight: placeholder.css("font-weight"), family: placeholder.css("font-family") }; axisCount = options.xaxes.length || 1; for (i = 0; i < axisCount; ++i) { axisOptions = options.xaxes[i]; if (axisOptions && !axisOptions.tickColor) { axisOptions.tickColor = axisOptions.color; } axisOptions = $.extend(true, {}, options.xaxis, axisOptions); options.xaxes[i] = axisOptions; if (axisOptions.font) { axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); if (!axisOptions.font.color) { axisOptions.font.color = axisOptions.color; } if (!axisOptions.font.lineHeight) { axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); } } } axisCount = options.yaxes.length || 1; for (i = 0; i < axisCount; ++i) { axisOptions = options.yaxes[i]; if (axisOptions && !axisOptions.tickColor) { axisOptions.tickColor = axisOptions.color; } axisOptions = $.extend(true, {}, options.yaxis, axisOptions); options.yaxes[i] = axisOptions; if (axisOptions.font) { axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); if (!axisOptions.font.color) { axisOptions.font.color = axisOptions.color; } if (!axisOptions.font.lineHeight) { axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); } } } // backwards compatibility, to be removed in future if (options.xaxis.noTicks && options.xaxis.ticks == null) options.xaxis.ticks = options.xaxis.noTicks; if (options.yaxis.noTicks && options.yaxis.ticks == null) options.yaxis.ticks = options.yaxis.noTicks; if (options.x2axis) { options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); options.xaxes[1].position = "top"; // Override the inherit to allow the axis to auto-scale if (options.x2axis.min == null) { options.xaxes[1].min = null; } if (options.x2axis.max == null) { options.xaxes[1].max = null; } } if (options.y2axis) { options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); options.yaxes[1].position = "right"; // Override the inherit to allow the axis to auto-scale if (options.y2axis.min == null) { options.yaxes[1].min = null; } if (options.y2axis.max == null) { options.yaxes[1].max = null; } } if (options.grid.coloredAreas) options.grid.markings = options.grid.coloredAreas; if (options.grid.coloredAreasColor) options.grid.markingsColor = options.grid.coloredAreasColor; if (options.lines) $.extend(true, options.series.lines, options.lines); if (options.points) $.extend(true, options.series.points, options.points); if (options.bars) $.extend(true, options.series.bars, options.bars); if (options.shadowSize != null) options.series.shadowSize = options.shadowSize; if (options.highlightColor != null) options.series.highlightColor = options.highlightColor; // save options on axes for future reference for (i = 0; i < options.xaxes.length; ++i) getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; for (i = 0; i < options.yaxes.length; ++i) getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; // add hooks from options for (var n in hooks) if (options.hooks[n] && options.hooks[n].length) hooks[n] = hooks[n].concat(options.hooks[n]); executeHooks(hooks.processOptions, [options]); } function setData(d) { series = parseData(d); fillInSeriesOptions(); processData(); } function parseData(d) { var res = []; for (var i = 0; i < d.length; ++i) { var s = $.extend(true, {}, options.series); if (d[i].data != null) { s.data = d[i].data; // move the data instead of deep-copy delete d[i].data; $.extend(true, s, d[i]); d[i].data = s.data; } else s.data = d[i]; res.push(s); } return res; } function axisNumber(obj, coord) { var a = obj[coord + "axis"]; if (typeof a == "object") // if we got a real axis, extract number a = a.n; if (typeof a != "number") a = 1; // default to first axis return a; } function allAxes() { // return flat array without annoying null entries return $.grep(xaxes.concat(yaxes), function (a) { return a; }); } function canvasToAxisCoords(pos) { // return an object with x/y corresponding to all used axes var res = {}, i, axis; for (i = 0; i < xaxes.length; ++i) { axis = xaxes[i]; if (axis && axis.used) res["x" + axis.n] = axis.c2p(pos.left); } for (i = 0; i < yaxes.length; ++i) { axis = yaxes[i]; if (axis && axis.used) res["y" + axis.n] = axis.c2p(pos.top); } if (res.x1 !== undefined) res.x = res.x1; if (res.y1 !== undefined) res.y = res.y1; return res; } function axisToCanvasCoords(pos) { // get canvas coords from the first pair of x/y found in pos var res = {}, i, axis, key; for (i = 0; i < xaxes.length; ++i) { axis = xaxes[i]; if (axis && axis.used) { key = "x" + axis.n; if (pos[key] == null && axis.n == 1) key = "x"; if (pos[key] != null) { res.left = axis.p2c(pos[key]); break; } } } for (i = 0; i < yaxes.length; ++i) { axis = yaxes[i]; if (axis && axis.used) { key = "y" + axis.n; if (pos[key] == null && axis.n == 1) key = "y"; if (pos[key] != null) { res.top = axis.p2c(pos[key]); break; } } } return res; } function getOrCreateAxis(axes, number) { if (!axes[number - 1]) axes[number - 1] = { n: number, // save the number for future reference direction: axes == xaxes ? "x" : "y", options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) }; return axes[number - 1]; } function fillInSeriesOptions() { var neededColors = series.length, maxIndex = -1, i; // Subtract the number of series that already have fixed colors or // color indexes from the number that we still need to generate. for (i = 0; i < series.length; ++i) { var sc = series[i].color; if (sc != null) { neededColors--; if (typeof sc == "number" && sc > maxIndex) { maxIndex = sc; } } } // If any of the series have fixed color indexes, then we need to // generate at least as many colors as the highest index. if (neededColors <= maxIndex) { neededColors = maxIndex + 1; } // Generate all the colors, using first the option colors and then // variations on those colors once they're exhausted. var c, colors = [], colorPool = options.colors, colorPoolSize = colorPool.length, variation = 0; for (i = 0; i < neededColors; i++) { c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); // Each time we exhaust the colors in the pool we adjust // a scaling factor used to produce more variations on // those colors. The factor alternates negative/positive // to produce lighter/darker colors. // Reset the variation after every few cycles, or else // it will end up producing only white or black colors. if (i % colorPoolSize == 0 && i) { if (variation >= 0) { if (variation < 0.5) { variation = -variation - 0.2; } else variation = 0; } else variation = -variation; } colors[i] = c.scale('rgb', 1 + variation); } // Finalize the series options, filling in their colors var colori = 0, s; for (i = 0; i < series.length; ++i) { s = series[i]; // assign colors if (s.color == null) { s.color = colors[colori].toString(); ++colori; } else if (typeof s.color == "number") s.color = colors[s.color].toString(); // turn on lines automatically in case nothing is set if (s.lines.show == null) { var v, show = true; for (v in s) if (s[v] && s[v].show) { show = false; break; } if (show) s.lines.show = true; } // If nothing was provided for lines.zero, default it to match // lines.fill, since areas by default should extend to zero. if (s.lines.zero == null) { s.lines.zero = !!s.lines.fill; } // setup axes s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); } } function processData() { var topSentry = Number.POSITIVE_INFINITY, bottomSentry = Number.NEGATIVE_INFINITY, fakeInfinity = Number.MAX_VALUE, i, j, k, m, length, s, points, ps, x, y, axis, val, f, p, data, format; function updateAxis(axis, min, max) { if (min < axis.datamin && min != -fakeInfinity) axis.datamin = min; if (max > axis.datamax && max != fakeInfinity) axis.datamax = max; } $.each(allAxes(), function (_, axis) { // init axis axis.datamin = topSentry; axis.datamax = bottomSentry; axis.used = false; }); for (i = 0; i < series.length; ++i) { s = series[i]; s.datapoints = { points: [] }; executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); } // first pass: clean and copy data for (i = 0; i < series.length; ++i) { s = series[i]; data = s.data; format = s.datapoints.format; if (!format) { format = []; // find out how to copy format.push({ x: true, number: true, required: true }); format.push({ y: true, number: true, required: true }); if (s.bars.show || (s.lines.show && s.lines.fill)) { var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); if (s.bars.horizontal) { delete format[format.length - 1].y; format[format.length - 1].x = true; } } s.datapoints.format = format; } if (s.datapoints.pointsize != null) continue; // already filled in s.datapoints.pointsize = format.length; ps = s.datapoints.pointsize; points = s.datapoints.points; var insertSteps = s.lines.show && s.lines.steps; s.xaxis.used = s.yaxis.used = true; for (j = k = 0; j < data.length; ++j, k += ps) { p = data[j]; var nullify = p == null; if (!nullify) { for (m = 0; m < ps; ++m) { val = p[m]; f = format[m]; if (f) { if (f.number && val != null) { val = +val; // convert to number if (isNaN(val)) val = null; else if (val == Infinity) val = fakeInfinity; else if (val == -Infinity) val = -fakeInfinity; } if (val == null) { if (f.required) nullify = true; if (f.defaultValue != null) val = f.defaultValue; } } points[k + m] = val; } } if (nullify) { for (m = 0; m < ps; ++m) { val = points[k + m]; if (val != null) { f = format[m]; // extract min/max info if (f.autoscale !== false) { if (f.x) { updateAxis(s.xaxis, val, val); } if (f.y) { updateAxis(s.yaxis, val, val); } } } points[k + m] = null; } } else { // a little bit of line specific stuff that // perhaps shouldn't be here, but lacking // better means... if (insertSteps && k > 0 && points[k - ps] != null && points[k - ps] != points[k] && points[k - ps + 1] != points[k + 1]) { // copy the point to make room for a middle point for (m = 0; m < ps; ++m) points[k + ps + m] = points[k + m]; // middle point has same y points[k + 1] = points[k - ps + 1]; // we've added a point, better reflect that k += ps; } } } } // give the hooks a chance to run for (i = 0; i < series.length; ++i) { s = series[i]; executeHooks(hooks.processDatapoints, [ s, s.datapoints]); } // second pass: find datamax/datamin for auto-scaling for (i = 0; i < series.length; ++i) { s = series[i]; points = s.datapoints.points; ps = s.datapoints.pointsize; format = s.datapoints.format; var xmin = topSentry, ymin = topSentry, xmax = bottomSentry, ymax = bottomSentry; for (j = 0; j < points.length; j += ps) { if (points[j] == null) continue; for (m = 0; m < ps; ++m) { val = points[j + m]; f = format[m]; if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) continue; if (f.x) { if (val < xmin) xmin = val; if (val > xmax) xmax = val; } if (f.y) { if (val < ymin) ymin = val; if (val > ymax) ymax = val; } } } if (s.bars.show) { // make sure we got room for the bar on the dancing floor var delta; switch (s.bars.align) { case "left": delta = 0; break; case "right": delta = -s.bars.barWidth; break; default: delta = -s.bars.barWidth / 2; } if (s.bars.horizontal) { ymin += delta; ymax += delta + s.bars.barWidth; } else { xmin += delta; xmax += delta + s.bars.barWidth; } } updateAxis(s.xaxis, xmin, xmax); updateAxis(s.yaxis, ymin, ymax); } $.each(allAxes(), function (_, axis) { if (axis.datamin == topSentry) axis.datamin = null; if (axis.datamax == bottomSentry) axis.datamax = null; }); } function setupCanvases() { // Make sure the placeholder is clear of everything except canvases // from a previous plot in this container that we'll try to re-use. placeholder.css("padding", 0) // padding messes up the positioning .children().filter(function(){ return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); }).remove(); if (placeholder.css("position") == 'static') placeholder.css("position", "relative"); // for positioning labels and overlay surface = new Canvas("flot-base", placeholder); overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features ctx = surface.context; octx = overlay.context; // define which element we're listening for events on eventHolder = $(overlay.element).unbind(); // If we're re-using a plot object, shut down the old one var existing = placeholder.data("plot"); if (existing) { existing.shutdown(); overlay.clear(); } // save in case we get replotted placeholder.data("plot", plot); } function bindEvents() { // bind events if (options.grid.hoverable) { eventHolder.mousemove(onMouseMove); // Use bind, rather than .mouseleave, because we officially // still support jQuery 1.2.6, which doesn't define a shortcut // for mouseenter or mouseleave. This was a bug/oversight that // was fixed somewhere around 1.3.x. We can return to using // .mouseleave when we drop support for 1.2.6. eventHolder.bind("mouseleave", onMouseLeave); } if (options.grid.clickable) eventHolder.click(onClick); executeHooks(hooks.bindEvents, [eventHolder]); } function shutdown() { if (redrawTimeout) clearTimeout(redrawTimeout); eventHolder.unbind("mousemove", onMouseMove); eventHolder.unbind("mouseleave", onMouseLeave); eventHolder.unbind("click", onClick); executeHooks(hooks.shutdown, [eventHolder]); } function setTransformationHelpers(axis) { // set helper functions on the axis, assumes plot area // has been computed already function identity(x) { return x; } var s, m, t = axis.options.transform || identity, it = axis.options.inverseTransform; // precompute how much the axis is scaling a point // in canvas space if (axis.direction == "x") { s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); m = Math.min(t(axis.max), t(axis.min)); } else { s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); s = -s; m = Math.max(t(axis.max), t(axis.min)); } // data point to canvas coordinate if (t == identity) // slight optimization axis.p2c = function (p) { return (p - m) * s; }; else axis.p2c = function (p) { return (t(p) - m) * s; }; // canvas coordinate to data point if (!it) axis.c2p = function (c) { return m + c / s; }; else axis.c2p = function (c) { return it(m + c / s); }; } function measureTickLabels(axis) { var opts = axis.options, ticks = axis.ticks || [], labelWidth = opts.labelWidth || 0, labelHeight = opts.labelHeight || 0, maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, font = opts.font || "flot-tick-label tickLabel"; for (var i = 0; i < ticks.length; ++i) { var t = ticks[i]; if (!t.label) continue; var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); labelWidth = Math.max(labelWidth, info.width); labelHeight = Math.max(labelHeight, info.height); } axis.labelWidth = opts.labelWidth || labelWidth; axis.labelHeight = opts.labelHeight || labelHeight; } function allocateAxisBoxFirstPhase(axis) { // find the bounding box of the axis by looking at label // widths/heights and ticks, make room by diminishing the // plotOffset; this first phase only looks at one // dimension per axis, the other dimension depends on the // other axes so will have to wait var lw = axis.labelWidth, lh = axis.labelHeight, pos = axis.options.position, isXAxis = axis.direction === "x", tickLength = axis.options.tickLength, axisMargin = options.grid.axisMargin, padding = options.grid.labelMargin, innermost = true, outermost = true, first = true, found = false; // Determine the axis's position in its direction and on its side $.each(isXAxis ? xaxes : yaxes, function(i, a) { if (a && (a.show || a.reserveSpace)) { if (a === axis) { found = true; } else if (a.options.position === pos) { if (found) { outermost = false; } else { innermost = false; } } if (!found) { first = false; } } }); // The outermost axis on each side has no margin if (outermost) { axisMargin = 0; } // The ticks for the first axis in each direction stretch across if (tickLength == null) { tickLength = first ? "full" : 5; } if (!isNaN(+tickLength)) padding += +tickLength; if (isXAxis) { lh += padding; if (pos == "bottom") { plotOffset.bottom += lh + axisMargin; axis.box = { top: surface.height - plotOffset.bottom, height: lh }; } else { axis.box = { top: plotOffset.top + axisMargin, height: lh }; plotOffset.top += lh + axisMargin; } } else { lw += padding; if (pos == "left") { axis.box = { left: plotOffset.left + axisMargin, width: lw }; plotOffset.left += lw + axisMargin; } else { plotOffset.right += lw + axisMargin; axis.box = { left: surface.width - plotOffset.right, width: lw }; } } // save for future reference axis.position = pos; axis.tickLength = tickLength; axis.box.padding = padding; axis.innermost = innermost; } function allocateAxisBoxSecondPhase(axis) { // now that all axis boxes have been placed in one // dimension, we can set the remaining dimension coordinates if (axis.direction == "x") { axis.box.left = plotOffset.left - axis.labelWidth / 2; axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; } else { axis.box.top = plotOffset.top - axis.labelHeight / 2; axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; } } function adjustLayoutForThingsStickingOut() { // possibly adjust plot offset to ensure everything stays // inside the canvas and isn't clipped off var minMargin = options.grid.minBorderMargin, axis, i; // check stuff from the plot (FIXME: this should just read // a value from the series, otherwise it's impossible to // customize) if (minMargin == null) { minMargin = 0; for (i = 0; i < series.length; ++i) minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); } var margins = { left: minMargin, right: minMargin, top: minMargin, bottom: minMargin }; // check axis labels, note we don't check the actual // labels but instead use the overall width/height to not // jump as much around with replots $.each(allAxes(), function (_, axis) { if (axis.reserveSpace && axis.ticks && axis.ticks.length) { if (axis.direction === "x") { margins.left = Math.max(margins.left, axis.labelWidth / 2); margins.right = Math.max(margins.right, axis.labelWidth / 2); } else { margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); margins.top = Math.max(margins.top, axis.labelHeight / 2); } } }); plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); } function setupGrid() { var i, axes = allAxes(), showGrid = options.grid.show; // Initialize the plot's offset from the edge of the canvas for (var a in plotOffset) { var margin = options.grid.margin || 0; plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; } executeHooks(hooks.processOffset, [plotOffset]); // If the grid is visible, add its border width to the offset for (var a in plotOffset) { if(typeof(options.grid.borderWidth) == "object") { plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; } else { plotOffset[a] += showGrid ? options.grid.borderWidth : 0; } } $.each(axes, function (_, axis) { var axisOpts = axis.options; axis.show = axisOpts.show == null ? axis.used : axisOpts.show; axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; setRange(axis); }); if (showGrid) { var allocatedAxes = $.grep(axes, function (axis) { return axis.show || axis.reserveSpace; }); $.each(allocatedAxes, function (_, axis) { // make the ticks setupTickGeneration(axis); setTicks(axis); snapRangeToTicks(axis, axis.ticks); // find labelWidth/Height for axis measureTickLabels(axis); }); // with all dimensions calculated, we can compute the // axis bounding boxes, start from the outside // (reverse order) for (i = allocatedAxes.length - 1; i >= 0; --i) allocateAxisBoxFirstPhase(allocatedAxes[i]); // make sure we've got enough space for things that // might stick out adjustLayoutForThingsStickingOut(); $.each(allocatedAxes, function (_, axis) { allocateAxisBoxSecondPhase(axis); }); } plotWidth = surface.width - plotOffset.left - plotOffset.right; plotHeight = surface.height - plotOffset.bottom - plotOffset.top; // now we got the proper plot dimensions, we can compute the scaling $.each(axes, function (_, axis) { setTransformationHelpers(axis); }); if (showGrid) { drawAxisLabels(); } insertLegend(); } function setRange(axis) { var opts = axis.options, min = +(opts.min != null ? opts.min : axis.datamin), max = +(opts.max != null ? opts.max : axis.datamax), delta = max - min; if (delta == 0.0) { // degenerate case var widen = max == 0 ? 1 : 0.01; if (opts.min == null) min -= widen; // always widen max if we couldn't widen min to ensure we // don't fall into min == max which doesn't work if (opts.max == null || opts.min != null) max += widen; } else { // consider autoscaling var margin = opts.autoscaleMargin; if (margin != null) { if (opts.min == null) { min -= delta * margin; // make sure we don't go below zero if all values // are positive if (min < 0 && axis.datamin != null && axis.datamin >= 0) min = 0; } if (opts.max == null) { max += delta * margin; if (max > 0 && axis.datamax != null && axis.datamax <= 0) max = 0; } } } axis.min = min; axis.max = max; } function setupTickGeneration(axis) { var opts = axis.options; // estimate number of ticks var noTicks; if (typeof opts.ticks == "number" && opts.ticks > 0) noTicks = opts.ticks; else // heuristic based on the model a*sqrt(x) fitted to // some data points that seemed reasonable noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); var delta = (axis.max - axis.min) / noTicks, dec = -Math.floor(Math.log(delta) / Math.LN10), maxDec = opts.tickDecimals; if (maxDec != null && dec > maxDec) { dec = maxDec; } var magn = Math.pow(10, -dec), norm = delta / magn, // norm is between 1.0 and 10.0 size; if (norm < 1.5) { size = 1; } else if (norm < 3) { size = 2; // special case for 2.5, requires an extra decimal if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { size = 2.5; ++dec; } } else if (norm < 7.5) { size = 5; } else { size = 10; } size *= magn; if (opts.minTickSize != null && size < opts.minTickSize) { size = opts.minTickSize; } axis.delta = delta; axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); axis.tickSize = opts.tickSize || size; // Time mode was moved to a plug-in in 0.8, and since so many people use it // we'll add an especially friendly reminder to make sure they included it. if (opts.mode == "time" && !axis.tickGenerator) { throw new Error("Time mode requires the flot.time plugin."); } // Flot supports base-10 axes; any other mode else is handled by a plug-in, // like flot.time.js. if (!axis.tickGenerator) { axis.tickGenerator = function (axis) { var ticks = [], start = floorInBase(axis.min, axis.tickSize), i = 0, v = Number.NaN, prev; do { prev = v; v = start + i * axis.tickSize; ticks.push(v); ++i; } while (v < axis.max && v != prev); return ticks; }; axis.tickFormatter = function (value, axis) { var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; var formatted = "" + Math.round(value * factor) / factor; // If tickDecimals was specified, ensure that we have exactly that // much precision; otherwise default to the value's own precision. if (axis.tickDecimals != null) { var decimal = formatted.indexOf("."); var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; if (precision < axis.tickDecimals) { return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); } } return formatted; }; } if ($.isFunction(opts.tickFormatter)) axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; if (opts.alignTicksWithAxis != null) { var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; if (otherAxis && otherAxis.used && otherAxis != axis) { // consider snapping min/max to outermost nice ticks var niceTicks = axis.tickGenerator(axis); if (niceTicks.length > 0) { if (opts.min == null) axis.min = Math.min(axis.min, niceTicks[0]); if (opts.max == null && niceTicks.length > 1) axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); } axis.tickGenerator = function (axis) { // copy ticks, scaled to this axis var ticks = [], v, i; for (i = 0; i < otherAxis.ticks.length; ++i) { v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); v = axis.min + v * (axis.max - axis.min); ticks.push(v); } return ticks; }; // we might need an extra decimal since forced // ticks don't necessarily fit naturally if (!axis.mode && opts.tickDecimals == null) { var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), ts = axis.tickGenerator(axis); // only proceed if the tick interval rounded // with an extra decimal doesn't give us a // zero at end if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) axis.tickDecimals = extraDec; } } } } function setTicks(axis) { var oticks = axis.options.ticks, ticks = []; if (oticks == null || (typeof oticks == "number" && oticks > 0)) ticks = axis.tickGenerator(axis); else if (oticks) { if ($.isFunction(oticks)) // generate the ticks ticks = oticks(axis); else ticks = oticks; } // clean up/labelify the supplied ticks, copy them over var i, v; axis.ticks = []; for (i = 0; i < ticks.length; ++i) { var label = null; var t = ticks[i]; if (typeof t == "object") { v = +t[0]; if (t.length > 1) label = t[1]; } else v = +t; if (label == null) label = axis.tickFormatter(v, axis); if (!isNaN(v)) axis.ticks.push({ v: v, label: label }); } } function snapRangeToTicks(axis, ticks) { if (axis.options.autoscaleMargin && ticks.length > 0) { // snap to ticks if (axis.options.min == null) axis.min = Math.min(axis.min, ticks[0].v); if (axis.options.max == null && ticks.length > 1) axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); } } function draw() { surface.clear(); executeHooks(hooks.drawBackground, [ctx]); var grid = options.grid; // draw background, if any if (grid.show && grid.backgroundColor) drawBackground(); if (grid.show && !grid.aboveData) { drawGrid(); } for (var i = 0; i < series.length; ++i) { executeHooks(hooks.drawSeries, [ctx, series[i]]); drawSeries(series[i]); } executeHooks(hooks.draw, [ctx]); if (grid.show && grid.aboveData) { drawGrid(); } surface.render(); // A draw implies that either the axes or data have changed, so we // should probably update the overlay highlights as well. triggerRedrawOverlay(); } function extractRange(ranges, coord) { var axis, from, to, key, axes = allAxes(); for (var i = 0; i < axes.length; ++i) { axis = axes[i]; if (axis.direction == coord) { key = coord + axis.n + "axis"; if (!ranges[key] && axis.n == 1) key = coord + "axis"; // support x1axis as xaxis if (ranges[key]) { from = ranges[key].from; to = ranges[key].to; break; } } } // backwards-compat stuff - to be removed in future if (!ranges[key]) { axis = coord == "x" ? xaxes[0] : yaxes[0]; from = ranges[coord + "1"]; to = ranges[coord + "2"]; } // auto-reverse as an added bonus if (from != null && to != null && from > to) { var tmp = from; from = to; to = tmp; } return { from: from, to: to, axis: axis }; } function drawBackground() { ctx.save(); ctx.translate(plotOffset.left, plotOffset.top); ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); ctx.fillRect(0, 0, plotWidth, plotHeight); ctx.restore(); } function drawGrid() { var i, axes, bw, bc; ctx.save(); ctx.translate(plotOffset.left, plotOffset.top); // draw markings var markings = options.grid.markings; if (markings) { if ($.isFunction(markings)) { axes = plot.getAxes(); // xmin etc. is backwards compatibility, to be // removed in the future axes.xmin = axes.xaxis.min; axes.xmax = axes.xaxis.max; axes.ymin = axes.yaxis.min; axes.ymax = axes.yaxis.max; markings = markings(axes); } for (i = 0; i < markings.length; ++i) { var m = markings[i], xrange = extractRange(m, "x"), yrange = extractRange(m, "y"); // fill in missing if (xrange.from == null) xrange.from = xrange.axis.min; if (xrange.to == null) xrange.to = xrange.axis.max; if (yrange.from == null) yrange.from = yrange.axis.min; if (yrange.to == null) yrange.to = yrange.axis.max; // clip if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) continue; xrange.from = Math.max(xrange.from, xrange.axis.min); xrange.to = Math.min(xrange.to, xrange.axis.max); yrange.from = Math.max(yrange.from, yrange.axis.min); yrange.to = Math.min(yrange.to, yrange.axis.max); var xequal = xrange.from === xrange.to, yequal = yrange.from === yrange.to; if (xequal && yequal) { continue; } // then draw xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); if (xequal || yequal) { var lineWidth = m.lineWidth || options.grid.markingsLineWidth, subPixel = lineWidth % 2 ? 0.5 : 0; ctx.beginPath(); ctx.strokeStyle = m.color || options.grid.markingsColor; ctx.lineWidth = lineWidth; if (xequal) { ctx.moveTo(xrange.to + subPixel, yrange.from); ctx.lineTo(xrange.to + subPixel, yrange.to); } else { ctx.moveTo(xrange.from, yrange.to + subPixel); ctx.lineTo(xrange.to, yrange.to + subPixel); } ctx.stroke(); } else { ctx.fillStyle = m.color || options.grid.markingsColor; ctx.fillRect(xrange.from, yrange.to, xrange.to - xrange.from, yrange.from - yrange.to); } } } // draw the ticks axes = allAxes(); bw = options.grid.borderWidth; for (var j = 0; j < axes.length; ++j) { var axis = axes[j], box = axis.box, t = axis.tickLength, x, y, xoff, yoff; if (!axis.show || axis.ticks.length == 0) continue; ctx.lineWidth = 1; // find the edges if (axis.direction == "x") { x = 0; if (t == "full") y = (axis.position == "top" ? 0 : plotHeight); else y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); } else { y = 0; if (t == "full") x = (axis.position == "left" ? 0 : plotWidth); else x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); } // draw tick bar if (!axis.innermost) { ctx.strokeStyle = axis.options.color; ctx.beginPath(); xoff = yoff = 0; if (axis.direction == "x") xoff = plotWidth + 1; else yoff = plotHeight + 1; if (ctx.lineWidth == 1) { if (axis.direction == "x") { y = Math.floor(y) + 0.5; } else { x = Math.floor(x) + 0.5; } } ctx.moveTo(x, y); ctx.lineTo(x + xoff, y + yoff); ctx.stroke(); } // draw ticks ctx.strokeStyle = axis.options.tickColor; ctx.beginPath(); for (i = 0; i < axis.ticks.length; ++i) { var v = axis.ticks[i].v; xoff = yoff = 0; if (isNaN(v) || v < axis.min || v > axis.max // skip those lying on the axes if we got a border || (t == "full" && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) && (v == axis.min || v == axis.max))) continue; if (axis.direction == "x") { x = axis.p2c(v); yoff = t == "full" ? -plotHeight : t; if (axis.position == "top") yoff = -yoff; } else { y = axis.p2c(v); xoff = t == "full" ? -plotWidth : t; if (axis.position == "left") xoff = -xoff; } if (ctx.lineWidth == 1) { if (axis.direction == "x") x = Math.floor(x) + 0.5; else y = Math.floor(y) + 0.5; } ctx.moveTo(x, y); ctx.lineTo(x + xoff, y + yoff); } ctx.stroke(); } // draw border if (bw) { // If either borderWidth or borderColor is an object, then draw the border // line by line instead of as one rectangle bc = options.grid.borderColor; if(typeof bw == "object" || typeof bc == "object") { if (typeof bw !== "object") { bw = {top: bw, right: bw, bottom: bw, left: bw}; } if (typeof bc !== "object") { bc = {top: bc, right: bc, bottom: bc, left: bc}; } if (bw.top > 0) { ctx.strokeStyle = bc.top; ctx.lineWidth = bw.top; ctx.beginPath(); ctx.moveTo(0 - bw.left, 0 - bw.top/2); ctx.lineTo(plotWidth, 0 - bw.top/2); ctx.stroke(); } if (bw.right > 0) { ctx.strokeStyle = bc.right; ctx.lineWidth = bw.right; ctx.beginPath(); ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); ctx.lineTo(plotWidth + bw.right / 2, plotHeight); ctx.stroke(); } if (bw.bottom > 0) { ctx.strokeStyle = bc.bottom; ctx.lineWidth = bw.bottom; ctx.beginPath(); ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); ctx.lineTo(0, plotHeight + bw.bottom / 2); ctx.stroke(); } if (bw.left > 0) { ctx.strokeStyle = bc.left; ctx.lineWidth = bw.left; ctx.beginPath(); ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); ctx.lineTo(0- bw.left/2, 0); ctx.stroke(); } } else { ctx.lineWidth = bw; ctx.strokeStyle = options.grid.borderColor; ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); } } ctx.restore(); } function drawAxisLabels() { $.each(allAxes(), function (_, axis) { var box = axis.box, legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, font = axis.options.font || "flot-tick-label tickLabel", tick, x, y, halign, valign; // Remove text before checking for axis.show and ticks.length; // otherwise plugins, like flot-tickrotor, that draw their own // tick labels will end up with both theirs and the defaults. surface.removeText(layer); if (!axis.show || axis.ticks.length == 0) return; for (var i = 0; i < axis.ticks.length; ++i) { tick = axis.ticks[i]; if (!tick.label || tick.v < axis.min || tick.v > axis.max) continue; if (axis.direction == "x") { halign = "center"; x = plotOffset.left + axis.p2c(tick.v); if (axis.position == "bottom") { y = box.top + box.padding; } else { y = box.top + box.height - box.padding; valign = "bottom"; } } else { valign = "middle"; y = plotOffset.top + axis.p2c(tick.v); if (axis.position == "left") { x = box.left + box.width - box.padding; halign = "right"; } else { x = box.left + box.padding; } } surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); } }); } function drawSeries(series) { if (series.lines.show) drawSeriesLines(series); if (series.bars.show) drawSeriesBars(series); if (series.points.show) drawSeriesPoints(series); } function drawSeriesLines(series) { function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { var points = datapoints.points, ps = datapoints.pointsize, prevx = null, prevy = null; ctx.beginPath(); for (var i = ps; i < points.length; i += ps) { var x1 = points[i - ps], y1 = points[i - ps + 1], x2 = points[i], y2 = points[i + 1]; if (x1 == null || x2 == null) continue; // clip with ymin if (y1 <= y2 && y1 < axisy.min) { if (y2 < axisy.min) continue; // line segment is outside // compute new intersection point x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; y1 = axisy.min; } else if (y2 <= y1 && y2 < axisy.min) { if (y1 < axisy.min) continue; x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; y2 = axisy.min; } // clip with ymax if (y1 >= y2 && y1 > axisy.max) { if (y2 > axisy.max) continue; x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; y1 = axisy.max; } else if (y2 >= y1 && y2 > axisy.max) { if (y1 > axisy.max) continue; x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; y2 = axisy.max; } // clip with xmin if (x1 <= x2 && x1 < axisx.min) { if (x2 < axisx.min) continue; y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; x1 = axisx.min; } else if (x2 <= x1 && x2 < axisx.min) { if (x1 < axisx.min) continue; y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; x2 = axisx.min; } // clip with xmax if (x1 >= x2 && x1 > axisx.max) { if (x2 > axisx.max) continue; y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; x1 = axisx.max; } else if (x2 >= x1 && x2 > axisx.max) { if (x1 > axisx.max) continue; y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; x2 = axisx.max; } if (x1 != prevx || y1 != prevy) ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); prevx = x2; prevy = y2; ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); } ctx.stroke(); } function plotLineArea(datapoints, axisx, axisy) { var points = datapoints.points, ps = datapoints.pointsize, bottom = Math.min(Math.max(0, axisy.min), axisy.max), i = 0, top, areaOpen = false, ypos = 1, segmentStart = 0, segmentEnd = 0; // we process each segment in two turns, first forward // direction to sketch out top, then once we hit the // end we go backwards to sketch the bottom while (true) { if (ps > 0 && i > points.length + ps) break; i += ps; // ps is negative if going backwards var x1 = points[i - ps], y1 = points[i - ps + ypos], x2 = points[i], y2 = points[i + ypos]; if (areaOpen) { if (ps > 0 && x1 != null && x2 == null) { // at turning point segmentEnd = i; ps = -ps; ypos = 2; continue; } if (ps < 0 && i == segmentStart + ps) { // done with the reverse sweep ctx.fill(); areaOpen = false; ps = -ps; ypos = 1; i = segmentStart = segmentEnd + ps; continue; } } if (x1 == null || x2 == null) continue; // clip x values // clip with xmin if (x1 <= x2 && x1 < axisx.min) { if (x2 < axisx.min) continue; y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; x1 = axisx.min; } else if (x2 <= x1 && x2 < axisx.min) { if (x1 < axisx.min) continue; y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; x2 = axisx.min; } // clip with xmax if (x1 >= x2 && x1 > axisx.max) { if (x2 > axisx.max) continue; y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; x1 = axisx.max; } else if (x2 >= x1 && x2 > axisx.max) { if (x1 > axisx.max) continue; y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; x2 = axisx.max; } if (!areaOpen) { // open area ctx.beginPath(); ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); areaOpen = true; } // now first check the case where both is outside if (y1 >= axisy.max && y2 >= axisy.max) { ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); continue; } else if (y1 <= axisy.min && y2 <= axisy.min) { ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); continue; } // else it's a bit more complicated, there might // be a flat maxed out rectangle first, then a // triangular cutout or reverse; to find these // keep track of the current x values var x1old = x1, x2old = x2; // clip the y values, without shortcutting, we // go through all cases in turn // clip with ymin if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; y1 = axisy.min; } else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; y2 = axisy.min; } // clip with ymax if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; y1 = axisy.max; } else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; y2 = axisy.max; } // if the x value was changed we got a rectangle // to fill if (x1 != x1old) { ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); // it goes to (x1, y1), but we fill that below } // fill triangular section, this sometimes result // in redundant points if (x1, y1) hasn't changed // from previous line to, but we just ignore that ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); // fill the other rectangle if it's there if (x2 != x2old) { ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); } } } ctx.save(); ctx.translate(plotOffset.left, plotOffset.top); ctx.lineJoin = "round"; var lw = series.lines.lineWidth, sw = series.shadowSize; // FIXME: consider another form of shadow when filling is turned on if (lw > 0 && sw > 0) { // draw shadow as a thick and thin line with transparency ctx.lineWidth = sw; ctx.strokeStyle = "rgba(0,0,0,0.1)"; // position shadow at angle from the mid of line var angle = Math.PI/18; plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); ctx.lineWidth = sw/2; plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); } ctx.lineWidth = lw; ctx.strokeStyle = series.color; var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); if (fillStyle) { ctx.fillStyle = fillStyle; plotLineArea(series.datapoints, series.xaxis, series.yaxis); } if (lw > 0) plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); ctx.restore(); } function drawSeriesPoints(series) { function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { var points = datapoints.points, ps = datapoints.pointsize; for (var i = 0; i < points.length; i += ps) { var x = points[i], y = points[i + 1]; if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) continue; ctx.beginPath(); x = axisx.p2c(x); y = axisy.p2c(y) + offset; if (symbol == "circle") ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); else symbol(ctx, x, y, radius, shadow); ctx.closePath(); if (fillStyle) { ctx.fillStyle = fillStyle; ctx.fill(); } ctx.stroke(); } } ctx.save(); ctx.translate(plotOffset.left, plotOffset.top); var lw = series.points.lineWidth, sw = series.shadowSize, radius = series.points.radius, symbol = series.points.symbol; // If the user sets the line width to 0, we change it to a very // small value. A line width of 0 seems to force the default of 1. // Doing the conditional here allows the shadow setting to still be // optional even with a lineWidth of 0. if( lw == 0 ) lw = 0.0001; if (lw > 0 && sw > 0) { // draw shadow in two steps var w = sw / 2; ctx.lineWidth = w; ctx.strokeStyle = "rgba(0,0,0,0.1)"; plotPoints(series.datapoints, radius, null, w + w/2, true, series.xaxis, series.yaxis, symbol); ctx.strokeStyle = "rgba(0,0,0,0.2)"; plotPoints(series.datapoints, radius, null, w/2, true, series.xaxis, series.yaxis, symbol); } ctx.lineWidth = lw; ctx.strokeStyle = series.color; plotPoints(series.datapoints, radius, getFillStyle(series.points, series.color), 0, false, series.xaxis, series.yaxis, symbol); ctx.restore(); } function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { var left, right, bottom, top, drawLeft, drawRight, drawTop, drawBottom, tmp; // in horizontal mode, we start the bar from the left // instead of from the bottom so it appears to be // horizontal rather than vertical if (horizontal) { drawBottom = drawRight = drawTop = true; drawLeft = false; left = b; right = x; top = y + barLeft; bottom = y + barRight; // account for negative bars if (right < left) { tmp = right; right = left; left = tmp; drawLeft = true; drawRight = false; } } else { drawLeft = drawRight = drawTop = true; drawBottom = false; left = x + barLeft; right = x + barRight; bottom = b; top = y; // account for negative bars if (top < bottom) { tmp = top; top = bottom; bottom = tmp; drawBottom = true; drawTop = false; } } // clip if (right < axisx.min || left > axisx.max || top < axisy.min || bottom > axisy.max) return; if (left < axisx.min) { left = axisx.min; drawLeft = false; } if (right > axisx.max) { right = axisx.max; drawRight = false; } if (bottom < axisy.min) { bottom = axisy.min; drawBottom = false; } if (top > axisy.max) { top = axisy.max; drawTop = false; } left = axisx.p2c(left); bottom = axisy.p2c(bottom); right = axisx.p2c(right); top = axisy.p2c(top); // fill the bar if (fillStyleCallback) { c.fillStyle = fillStyleCallback(bottom, top); c.fillRect(left, top, right - left, bottom - top) } // draw outline if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { c.beginPath(); // FIXME: inline moveTo is buggy with excanvas c.moveTo(left, bottom); if (drawLeft) c.lineTo(left, top); else c.moveTo(left, top); if (drawTop) c.lineTo(right, top); else c.moveTo(right, top); if (drawRight) c.lineTo(right, bottom); else c.moveTo(right, bottom); if (drawBottom) c.lineTo(left, bottom); else c.moveTo(left, bottom); c.stroke(); } } function drawSeriesBars(series) { function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { var points = datapoints.points, ps = datapoints.pointsize; for (var i = 0; i < points.length; i += ps) { if (points[i] == null) continue; drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); } } ctx.save(); ctx.translate(plotOffset.left, plotOffset.top); // FIXME: figure out a way to add shadows (for instance along the right edge) ctx.lineWidth = series.bars.lineWidth; ctx.strokeStyle = series.color; var barLeft; switch (series.bars.align) { case "left": barLeft = 0; break; case "right": barLeft = -series.bars.barWidth; break; default: barLeft = -series.bars.barWidth / 2; } var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); ctx.restore(); } function getFillStyle(filloptions, seriesColor, bottom, top) { var fill = filloptions.fill; if (!fill) return null; if (filloptions.fillColor) return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); var c = $.color.parse(seriesColor); c.a = typeof fill == "number" ? fill : 0.4; c.normalize(); return c.toString(); } function insertLegend() { if (options.legend.container != null) { $(options.legend.container).html(""); } else { placeholder.find(".legend").remove(); } if (!options.legend.show) { return; } var fragments = [], entries = [], rowStarted = false, lf = options.legend.labelFormatter, s, label; // Build a list of legend entries, with each having a label and a color for (var i = 0; i < series.length; ++i) { s = series[i]; if (s.label) { label = lf ? lf(s.label, s) : s.label; if (label) { entries.push({ label: label, color: s.color }); } } } // Sort the legend using either the default or a custom comparator if (options.legend.sorted) { if ($.isFunction(options.legend.sorted)) { entries.sort(options.legend.sorted); } else if (options.legend.sorted == "reverse") { entries.reverse(); } else { var ascending = options.legend.sorted != "descending"; entries.sort(function(a, b) { return a.label == b.label ? 0 : ( (a.label < b.label) != ascending ? 1 : -1 // Logical XOR ); }); } } // Generate markup for the list of entries, in their final order for (var i = 0; i < entries.length; ++i) { var entry = entries[i]; if (i % options.legend.noColumns == 0) { if (rowStarted) fragments.push('</tr>'); fragments.push('<tr>'); rowStarted = true; } fragments.push( '<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + entry.color + ';overflow:hidden"></div></div></td>' + '<td class="legendLabel">' + entry.label + '</td>' ); } if (rowStarted) fragments.push('</tr>'); if (fragments.length == 0) return; var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>'; if (options.legend.container != null) $(options.legend.container).html(table); else { var pos = "", p = options.legend.position, m = options.legend.margin; if (m[0] == null) m = [m, m]; if (p.charAt(0) == "n") pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; else if (p.charAt(0) == "s") pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; if (p.charAt(1) == "e") pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; else if (p.charAt(1) == "w") pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder); if (options.legend.backgroundOpacity != 0.0) { // put in the transparent background // separately to avoid blended labels and // label boxes var c = options.legend.backgroundColor; if (c == null) { c = options.grid.backgroundColor; if (c && typeof c == "string") c = $.color.parse(c); else c = $.color.extract(legend, 'background-color'); c.a = 1; c = c.toString(); } var div = legend.children(); $('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity); } } } // interactive features var highlights = [], redrawTimeout = null; // returns the data item the mouse is over, or null if none is found function findNearbyItem(mouseX, mouseY, seriesFilter) { var maxDistance = options.grid.mouseActiveRadius, smallestDistance = maxDistance * maxDistance + 1, item = null, foundPoint = false, i, j, ps; for (i = series.length - 1; i >= 0; --i) { if (!seriesFilter(series[i])) continue; var s = series[i], axisx = s.xaxis, axisy = s.yaxis, points = s.datapoints.points, mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster my = axisy.c2p(mouseY), maxx = maxDistance / axisx.scale, maxy = maxDistance / axisy.scale; ps = s.datapoints.pointsize; // with inverse transforms, we can't use the maxx/maxy // optimization, sadly if (axisx.options.inverseTransform) maxx = Number.MAX_VALUE; if (axisy.options.inverseTransform) maxy = Number.MAX_VALUE; if (s.lines.show || s.points.show) { for (j = 0; j < points.length; j += ps) { var x = points[j], y = points[j + 1]; if (x == null) continue; // For points and lines, the cursor must be within a // certain distance to the data point if (x - mx > maxx || x - mx < -maxx || y - my > maxy || y - my < -maxy) continue; // We have to calculate distances in pixels, not in // data units, because the scales of the axes may be different var dx = Math.abs(axisx.p2c(x) - mouseX), dy = Math.abs(axisy.p2c(y) - mouseY), dist = dx * dx + dy * dy; // we save the sqrt // use <= to ensure last point takes precedence // (last generally means on top of) if (dist < smallestDistance) { smallestDistance = dist; item = [i, j / ps]; } } } if (s.bars.show && !item) { // no other point can be nearby var barLeft, barRight; switch (s.bars.align) { case "left": barLeft = 0; break; case "right": barLeft = -s.bars.barWidth; break; default: barLeft = -s.bars.barWidth / 2; } barRight = barLeft + s.bars.barWidth; for (j = 0; j < points.length; j += ps) { var x = points[j], y = points[j + 1], b = points[j + 2]; if (x == null) continue; // for a bar graph, the cursor must be inside the bar if (series[i].bars.horizontal ? (mx <= Math.max(b, x) && mx >= Math.min(b, x) && my >= y + barLeft && my <= y + barRight) : (mx >= x + barLeft && mx <= x + barRight && my >= Math.min(b, y) && my <= Math.max(b, y))) item = [i, j / ps]; } } } if (item) { i = item[0]; j = item[1]; ps = series[i].datapoints.pointsize; return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), dataIndex: j, series: series[i], seriesIndex: i }; } return null; } function onMouseMove(e) { if (options.grid.hoverable) triggerClickHoverEvent("plothover", e, function (s) { return s["hoverable"] != false; }); } function onMouseLeave(e) { if (options.grid.hoverable) triggerClickHoverEvent("plothover", e, function (s) { return false; }); } function onClick(e) { triggerClickHoverEvent("plotclick", e, function (s) { return s["clickable"] != false; }); } // trigger click or hover event (they send the same parameters // so we share their code) function triggerClickHoverEvent(eventname, event, seriesFilter) { var offset = eventHolder.offset(), canvasX = event.pageX - offset.left - plotOffset.left, canvasY = event.pageY - offset.top - plotOffset.top, pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); pos.pageX = event.pageX; pos.pageY = event.pageY; var item = findNearbyItem(canvasX, canvasY, seriesFilter); if (item) { // fill in mouse pos for any listeners out there item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); } if (options.grid.autoHighlight) { // clear auto-highlights for (var i = 0; i < highlights.length; ++i) { var h = highlights[i]; if (h.auto == eventname && !(item && h.series == item.series && h.point[0] == item.datapoint[0] && h.point[1] == item.datapoint[1])) unhighlight(h.series, h.point); } if (item) highlight(item.series, item.datapoint, eventname); } placeholder.trigger(eventname, [ pos, item ]); } function triggerRedrawOverlay() { var t = options.interaction.redrawOverlayInterval; if (t == -1) { // skip event queue drawOverlay(); return; } if (!redrawTimeout) redrawTimeout = setTimeout(drawOverlay, t); } function drawOverlay() { redrawTimeout = null; // draw highlights octx.save(); overlay.clear(); octx.translate(plotOffset.left, plotOffset.top); var i, hi; for (i = 0; i < highlights.length; ++i) { hi = highlights[i]; if (hi.series.bars.show) drawBarHighlight(hi.series, hi.point); else drawPointHighlight(hi.series, hi.point); } octx.restore(); executeHooks(hooks.drawOverlay, [octx]); } function highlight(s, point, auto) { if (typeof s == "number") s = series[s]; if (typeof point == "number") { var ps = s.datapoints.pointsize; point = s.datapoints.points.slice(ps * point, ps * (point + 1)); } var i = indexOfHighlight(s, point); if (i == -1) { highlights.push({ series: s, point: point, auto: auto }); triggerRedrawOverlay(); } else if (!auto) highlights[i].auto = false; } function unhighlight(s, point) { if (s == null && point == null) { highlights = []; triggerRedrawOverlay(); return; } if (typeof s == "number") s = series[s]; if (typeof point == "number") { var ps = s.datapoints.pointsize; point = s.datapoints.points.slice(ps * point, ps * (point + 1)); } var i = indexOfHighlight(s, point); if (i != -1) { highlights.splice(i, 1); triggerRedrawOverlay(); } } function indexOfHighlight(s, p) { for (var i = 0; i < highlights.length; ++i) { var h = highlights[i]; if (h.series == s && h.point[0] == p[0] && h.point[1] == p[1]) return i; } return -1; } function drawPointHighlight(series, point) { var x = point[0], y = point[1], axisx = series.xaxis, axisy = series.yaxis, highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) return; var pointRadius = series.points.radius + series.points.lineWidth / 2; octx.lineWidth = pointRadius; octx.strokeStyle = highlightColor; var radius = 1.5 * pointRadius; x = axisx.p2c(x); y = axisy.p2c(y); octx.beginPath(); if (series.points.symbol == "circle") octx.arc(x, y, radius, 0, 2 * Math.PI, false); else series.points.symbol(octx, x, y, radius, false); octx.closePath(); octx.stroke(); } function drawBarHighlight(series, point) { var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), fillStyle = highlightColor, barLeft; switch (series.bars.align) { case "left": barLeft = 0; break; case "right": barLeft = -series.bars.barWidth; break; default: barLeft = -series.bars.barWidth / 2; } octx.lineWidth = series.bars.lineWidth; octx.strokeStyle = highlightColor; drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); } function getColorOrGradient(spec, bottom, top, defaultColor) { if (typeof spec == "string") return spec; else { // assume this is a gradient spec; IE currently only // supports a simple vertical gradient properly, so that's // what we support too var gradient = ctx.createLinearGradient(0, top, 0, bottom); for (var i = 0, l = spec.colors.length; i < l; ++i) { var c = spec.colors[i]; if (typeof c != "string") { var co = $.color.parse(defaultColor); if (c.brightness != null) co = co.scale('rgb', c.brightness); if (c.opacity != null) co.a *= c.opacity; c = co.toString(); } gradient.addColorStop(i / (l - 1), c); } return gradient; } } } // Add the plot function to the top level of the jQuery object $.plot = function(placeholder, data, options) { //var t0 = new Date(); var plot = new Plot($(placeholder), data, options, $.plot.plugins); //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); return plot; }; $.plot.version = "0.8.3"; $.plot.plugins = []; // Also add the plot function as a chainable property $.fn.plot = function(data, options) { return this.each(function() { $.plot(this, data, options); }); }; // round to nearby lower multiple of base function floorInBase(n, base) { return base * Math.floor(n / base); } })(jQuery); flot/flot-stack.js 0000644 00000015424 15030376016 0010124 0 ustar 00 /* Flot plugin for stacking data sets rather than overlyaing them. Copyright (c) 2007-2014 IOLA and Ole Laursen. Licensed under the MIT license. The plugin assumes the data is sorted on x (or y if stacking horizontally). For line charts, it is assumed that if a line has an undefined gap (from a null point), then the line above it should have the same gap - insert zeros instead of "null" if you want another behaviour. This also holds for the start and end of the chart. Note that stacking a mix of positive and negative values in most instances doesn't make sense (so it looks weird). Two or more series are stacked when their "stack" attribute is set to the same key (which can be any number or string or just "true"). To specify the default stack, you can set the stack option like this: series: { stack: null/false, true, or a key (number/string) } You can also specify it for a single series, like this: $.plot( $("#placeholder"), [{ data: [ ... ], stack: true }]) The stacking order is determined by the order of the data series in the array (later series end up on top of the previous). Internally, the plugin modifies the datapoints in each series, adding an offset to the y value. For line series, extra data points are inserted through interpolation. If there's a second y value, it's also adjusted (e.g for bar charts or filled areas). */ (function ($) { var options = { series: { stack: null } // or number/string }; function init(plot) { function findMatchingSeries(s, allseries) { var res = null; for (var i = 0; i < allseries.length; ++i) { if (s == allseries[i]) break; if (allseries[i].stack == s.stack) res = allseries[i]; } return res; } function stackData(plot, s, datapoints) { if (s.stack == null || s.stack === false) return; var other = findMatchingSeries(s, plot.getData()); if (!other) return; var ps = datapoints.pointsize, points = datapoints.points, otherps = other.datapoints.pointsize, otherpoints = other.datapoints.points, newpoints = [], px, py, intery, qx, qy, bottom, withlines = s.lines.show, horizontal = s.bars.horizontal, withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), withsteps = withlines && s.lines.steps, fromgap = true, keyOffset = horizontal ? 1 : 0, accumulateOffset = horizontal ? 0 : 1, i = 0, j = 0, l, m; while (true) { if (i >= points.length) break; l = newpoints.length; if (points[i] == null) { // copy gaps for (m = 0; m < ps; ++m) newpoints.push(points[i + m]); i += ps; } else if (j >= otherpoints.length) { // for lines, we can't use the rest of the points if (!withlines) { for (m = 0; m < ps; ++m) newpoints.push(points[i + m]); } i += ps; } else if (otherpoints[j] == null) { // oops, got a gap for (m = 0; m < ps; ++m) newpoints.push(null); fromgap = true; j += otherps; } else { // cases where we actually got two points px = points[i + keyOffset]; py = points[i + accumulateOffset]; qx = otherpoints[j + keyOffset]; qy = otherpoints[j + accumulateOffset]; bottom = 0; if (px == qx) { for (m = 0; m < ps; ++m) newpoints.push(points[i + m]); newpoints[l + accumulateOffset] += qy; bottom = qy; i += ps; j += otherps; } else if (px > qx) { // we got past point below, might need to // insert interpolated extra point if (withlines && i > 0 && points[i - ps] != null) { intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); newpoints.push(qx); newpoints.push(intery + qy); for (m = 2; m < ps; ++m) newpoints.push(points[i + m]); bottom = qy; } j += otherps; } else { // px < qx if (fromgap && withlines) { // if we come from a gap, we just skip this point i += ps; continue; } for (m = 0; m < ps; ++m) newpoints.push(points[i + m]); // we might be able to interpolate a point below, // this can give us a better y if (withlines && j > 0 && otherpoints[j - otherps] != null) bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); newpoints[l + accumulateOffset] += bottom; i += ps; } fromgap = false; if (l != newpoints.length && withbottom) newpoints[l + 2] += bottom; } // maintain the line steps invariant if (withsteps && l != newpoints.length && l > 0 && newpoints[l] != null && newpoints[l] != newpoints[l - ps] && newpoints[l + 1] != newpoints[l - ps + 1]) { for (m = 0; m < ps; ++m) newpoints[l + ps + m] = newpoints[l + m]; newpoints[l + 1] = newpoints[l - ps + 1]; } } datapoints.points = newpoints; } plot.hooks.processDatapoints.push(stackData); } $.plot.plugins.push({ init: init, options: options, name: 'stack', version: '1.2' }); })(jQuery); flot/flot-demo-1.js 0000644 00000007040 15030376016 0010074 0 ustar 00 $(function() { //Interacting with Data Points example var sin = [], cos = []; for (var i = 0; i < 354; i += 31) { sin.push([i, Math.random(i)]); cos.push([i, Math.random(i)]); } var plot = $.plot($("#data-example-1"), [{ data: sin, label: "Today" }, { data: cos, label: "Yesterday" }], { series: { shadowSize: 0, lines: { show: true, lineWidth: 2 }, points: { show: true } }, grid: { labelMargin: 10, hoverable: true, clickable: true, borderWidth: 1, borderColor: 'rgba(82, 167, 224, 0.06)' }, legend: { backgroundColor: '#fff' }, yaxis: { tickColor: 'rgba(0, 0, 0, 0.06)', font: {color: 'rgba(0, 0, 0, 0.4)'}}, xaxis: { tickColor: 'rgba(0, 0, 0, 0.06)', font: {color: 'rgba(0, 0, 0, 0.4)'}}, colors: [getUIColor('success'), getUIColor('gray')], tooltip: true, tooltipOpts: { content: "x: %x, y: %y" } }); var previousPoint = null; $("#data-example-1").bind("plothover", function (event, pos, item) { $("#x").text(pos.x.toFixed(2)); $("#y").text(pos.y.toFixed(2)); }); $("#data-example-1").bind("plotclick", function (event, pos, item) { if (item) { $("#clickdata").text("You clicked point " + item.dataIndex + " in " + item.series.label + "."); plot.highlight(item.series, item.datapoint); } }); }); $(function() { // We use an inline data source in the example, usually data would // be fetched from a server var data = [], totalPoints = 300; function getRandomData() { if (data.length > 0) data = data.slice(1); // Do a random walk while (data.length < totalPoints) { var prev = data.length > 0 ? data[data.length - 1] : 50, y = prev + Math.random() * 10 - 5; if (y < 0) { y = 0; } else if (y > 100) { y = 100; } data.push(y); } // Zip the generated y values with the x values var res = []; for (var i = 0; i < data.length; ++i) { res.push([i, data[i]]) } return res; } // Set up the control widget var updateInterval = 30; var plot = $.plot("#data-example-3", [ getRandomData() ], { series: { lines: { show: true, lineWidth: 2, fill: 0.5, fillColor: { colors: [ { opacity: 0.01 }, { opacity: 0.08 } ] } }, shadowSize: 0 // Drawing is faster without shadows }, grid: { labelMargin: 10, hoverable: true, clickable: true, borderWidth: 1, borderColor: 'rgba(82, 167, 224, 0.06)' }, yaxis: { min: 0, max: 120, tickColor: 'rgba(0, 0, 0, 0.06)', font: {color: 'rgba(0, 0, 0, 0.4)'}}, xaxis: { show: false }, colors: [getUIColor('default'),getUIColor('gray')] }); function update() { plot.setData([getRandomData()]); // Since the axes don't change, we don't need to call plot.setupGrid() plot.draw(); setTimeout(update, updateInterval); } update(); }); flot/flot-pie.js 0000644 00000074727 15030376016 0007607 0 ustar 00 /* Flot plugin for rendering pie charts. Copyright (c) 2007-2014 IOLA and Ole Laursen. Licensed under the MIT license. The plugin assumes that each series has a single data value, and that each value is a positive integer or zero. Negative numbers don't make sense for a pie chart, and have unpredictable results. The values do NOT need to be passed in as percentages; the plugin will calculate the total and per-slice percentages internally. * Created by Brian Medendorp * Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars The plugin supports these options: series: { pie: { show: true/false radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto' innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show) offset: { top: integer value to move the pie up or down left: integer value to move the pie left or right, or 'auto' }, stroke: { color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF') width: integer pixel width of the stroke }, label: { show: true/false, or 'auto' formatter: a user-defined function that modifies the text/style of the label text radius: 0-1 for percentage of fullsize, or a specified pixel length background: { color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000') opacity: 0-1 }, threshold: 0-1 for the percentage value at which to hide labels (if they're too small) }, combine: { threshold: 0-1 for the percentage value at which to combine slices (if they're too small) color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined label: any text value of what the combined slice should be labeled } highlight: { opacity: 0-1 } } } More detail and specific examples can be found in the included HTML file. */ (function($) { // Maximum redraw attempts when fitting labels within the plot var REDRAW_ATTEMPTS = 10; // Factor by which to shrink the pie when fitting labels within the plot var REDRAW_SHRINK = 0.95; function init(plot) { var canvas = null, target = null, options = null, maxRadius = null, centerLeft = null, centerTop = null, processed = false, ctx = null; // interactive variables var highlights = []; // add hook to determine if pie plugin in enabled, and then perform necessary operations plot.hooks.processOptions.push(function(plot, options) { if (options.series.pie.show) { options.grid.show = false; // set labels.show if (options.series.pie.label.show == "auto") { if (options.legend.show) { options.series.pie.label.show = false; } else { options.series.pie.label.show = true; } } // set radius if (options.series.pie.radius == "auto") { if (options.series.pie.label.show) { options.series.pie.radius = 3/4; } else { options.series.pie.radius = 1; } } // ensure sane tilt if (options.series.pie.tilt > 1) { options.series.pie.tilt = 1; } else if (options.series.pie.tilt < 0) { options.series.pie.tilt = 0; } } }); plot.hooks.bindEvents.push(function(plot, eventHolder) { var options = plot.getOptions(); if (options.series.pie.show) { if (options.grid.hoverable) { eventHolder.unbind("mousemove").mousemove(onMouseMove); } if (options.grid.clickable) { eventHolder.unbind("click").click(onClick); } } }); plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) { var options = plot.getOptions(); if (options.series.pie.show) { processDatapoints(plot, series, data, datapoints); } }); plot.hooks.drawOverlay.push(function(plot, octx) { var options = plot.getOptions(); if (options.series.pie.show) { drawOverlay(plot, octx); } }); plot.hooks.draw.push(function(plot, newCtx) { var options = plot.getOptions(); if (options.series.pie.show) { draw(plot, newCtx); } }); function processDatapoints(plot, series, datapoints) { if (!processed) { processed = true; canvas = plot.getCanvas(); target = $(canvas).parent(); options = plot.getOptions(); plot.setData(combine(plot.getData())); } } function combine(data) { var total = 0, combined = 0, numCombined = 0, color = options.series.pie.combine.color, newdata = []; // Fix up the raw data from Flot, ensuring the data is numeric for (var i = 0; i < data.length; ++i) { var value = data[i].data; // If the data is an array, we'll assume that it's a standard // Flot x-y pair, and are concerned only with the second value. // Note how we use the original array, rather than creating a // new one; this is more efficient and preserves any extra data // that the user may have stored in higher indexes. if ($.isArray(value) && value.length == 1) { value = value[0]; } if ($.isArray(value)) { // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { value[1] = +value[1]; } else { value[1] = 0; } } else if (!isNaN(parseFloat(value)) && isFinite(value)) { value = [1, +value]; } else { value = [1, 0]; } data[i].data = [value]; } // Sum up all the slices, so we can calculate percentages for each for (var i = 0; i < data.length; ++i) { total += data[i].data[0][1]; } // Count the number of slices with percentages below the combine // threshold; if it turns out to be just one, we won't combine. for (var i = 0; i < data.length; ++i) { var value = data[i].data[0][1]; if (value / total <= options.series.pie.combine.threshold) { combined += value; numCombined++; if (!color) { color = data[i].color; } } } for (var i = 0; i < data.length; ++i) { var value = data[i].data[0][1]; if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { newdata.push( $.extend(data[i], { /* extend to allow keeping all other original data values and using them e.g. in labelFormatter. */ data: [[1, value]], color: data[i].color, label: data[i].label, angle: value * Math.PI * 2 / total, percent: value / (total / 100) }) ); } } if (numCombined > 1) { newdata.push({ data: [[1, combined]], color: color, label: options.series.pie.combine.label, angle: combined * Math.PI * 2 / total, percent: combined / (total / 100) }); } return newdata; } function draw(plot, newCtx) { if (!target) { return; // if no series were passed } var canvasWidth = plot.getPlaceholder().width(), canvasHeight = plot.getPlaceholder().height(), legendWidth = target.children().filter(".legend").children().width() || 0; ctx = newCtx; // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! // When combining smaller slices into an 'other' slice, we need to // add a new series. Since Flot gives plugins no way to modify the // list of series, the pie plugin uses a hack where the first call // to processDatapoints results in a call to setData with the new // list of series, then subsequent processDatapoints do nothing. // The plugin-global 'processed' flag is used to control this hack; // it starts out false, and is set to true after the first call to // processDatapoints. // Unfortunately this turns future setData calls into no-ops; they // call processDatapoints, the flag is true, and nothing happens. // To fix this we'll set the flag back to false here in draw, when // all series have been processed, so the next sequence of calls to // processDatapoints once again starts out with a slice-combine. // This is really a hack; in 0.9 we need to give plugins a proper // way to modify series before any processing begins. processed = false; // calculate maximum radius and center point maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; centerTop = canvasHeight / 2 + options.series.pie.offset.top; centerLeft = canvasWidth / 2; if (options.series.pie.offset.left == "auto") { if (options.legend.position.match("w")) { centerLeft += legendWidth / 2; } else { centerLeft -= legendWidth / 2; } if (centerLeft < maxRadius) { centerLeft = maxRadius; } else if (centerLeft > canvasWidth - maxRadius) { centerLeft = canvasWidth - maxRadius; } } else { centerLeft += options.series.pie.offset.left; } var slices = plot.getData(), attempts = 0; // Keep shrinking the pie's radius until drawPie returns true, // indicating that all the labels fit, or we try too many times. do { if (attempts > 0) { maxRadius *= REDRAW_SHRINK; } attempts += 1; clear(); if (options.series.pie.tilt <= 0.8) { drawShadow(); } } while (!drawPie() && attempts < REDRAW_ATTEMPTS) if (attempts >= REDRAW_ATTEMPTS) { clear(); target.prepend("<div class='error'>Could not draw pie with labels contained inside canvas</div>"); } if (plot.setSeries && plot.insertLegend) { plot.setSeries(slices); plot.insertLegend(); } // we're actually done at this point, just defining internal functions at this point function clear() { ctx.clearRect(0, 0, canvasWidth, canvasHeight); target.children().filter(".pieLabel, .pieLabelBackground").remove(); } function drawShadow() { var shadowLeft = options.series.pie.shadow.left; var shadowTop = options.series.pie.shadow.top; var edge = 10; var alpha = options.series.pie.shadow.alpha; var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) { return; // shadow would be outside canvas, so don't draw it } ctx.save(); ctx.translate(shadowLeft,shadowTop); ctx.globalAlpha = alpha; ctx.fillStyle = "#000"; // center and rotate to starting position ctx.translate(centerLeft,centerTop); ctx.scale(1, options.series.pie.tilt); //radius -= edge; for (var i = 1; i <= edge; i++) { ctx.beginPath(); ctx.arc(0, 0, radius, 0, Math.PI * 2, false); ctx.fill(); radius -= i; } ctx.restore(); } function drawPie() { var startAngle = Math.PI * options.series.pie.startAngle; var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; // center and rotate to starting position ctx.save(); ctx.translate(centerLeft,centerTop); ctx.scale(1, options.series.pie.tilt); //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera // draw slices ctx.save(); var currentAngle = startAngle; for (var i = 0; i < slices.length; ++i) { slices[i].startAngle = currentAngle; drawSlice(slices[i].angle, slices[i].color, true); } ctx.restore(); // draw slice outlines if (options.series.pie.stroke.width > 0) { ctx.save(); ctx.lineWidth = options.series.pie.stroke.width; currentAngle = startAngle; for (var i = 0; i < slices.length; ++i) { drawSlice(slices[i].angle, options.series.pie.stroke.color, false); } ctx.restore(); } // draw donut hole drawDonutHole(ctx); ctx.restore(); // Draw the labels, returning true if they fit within the plot if (options.series.pie.label.show) { return drawLabels(); } else return true; function drawSlice(angle, color, fill) { if (angle <= 0 || isNaN(angle)) { return; } if (fill) { ctx.fillStyle = color; } else { ctx.strokeStyle = color; ctx.lineJoin = "round"; } ctx.beginPath(); if (Math.abs(angle - Math.PI * 2) > 0.000000001) { ctx.moveTo(0, 0); // Center of the pie } //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false); ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false); ctx.closePath(); //ctx.rotate(angle); // This doesn't work properly in Opera currentAngle += angle; if (fill) { ctx.fill(); } else { ctx.stroke(); } } function drawLabels() { var currentAngle = startAngle; var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius; for (var i = 0; i < slices.length; ++i) { if (slices[i].percent >= options.series.pie.label.threshold * 100) { if (!drawLabel(slices[i], currentAngle, i)) { return false; } } currentAngle += slices[i].angle; } return true; function drawLabel(slice, startAngle, index) { if (slice.data[0][1] == 0) { return true; } // format label text var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; if (lf) { text = lf(slice.label, slice); } else { text = slice.label; } if (plf) { text = plf(text, slice); } var halfAngle = ((startAngle + slice.angle) + startAngle) / 2; var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; var html = "<span class='pieLabel' id='pieLabel" + index + "' style='position:absolute;top:" + y + "px;left:" + x + "px;'>" + text + "</span>"; target.append(html); var label = target.children("#pieLabel" + index); var labelTop = (y - label.height() / 2); var labelLeft = (x - label.width() / 2); label.css("top", labelTop); label.css("left", labelLeft); // check to make sure that the label is not outside the canvas if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { return false; } if (options.series.pie.label.background.opacity != 0) { // put in the transparent background separately to avoid blended labels and label boxes var c = options.series.pie.label.background.color; if (c == null) { c = slice.color; } var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;"; $("<div class='pieLabelBackground' style='position:absolute;width:" + label.width() + "px;height:" + label.height() + "px;" + pos + "background-color:" + c + ";'></div>") .css("opacity", options.series.pie.label.background.opacity) .insertBefore(label); } return true; } // end individual label function } // end drawLabels function } // end drawPie function } // end draw function // Placed here because it needs to be accessed from multiple locations function drawDonutHole(layer) { if (options.series.pie.innerRadius > 0) { // subtract the center layer.save(); var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color layer.beginPath(); layer.fillStyle = options.series.pie.stroke.color; layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); layer.fill(); layer.closePath(); layer.restore(); // add inner stroke layer.save(); layer.beginPath(); layer.strokeStyle = options.series.pie.stroke.color; layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); layer.stroke(); layer.closePath(); layer.restore(); // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. } } //-- Additional Interactive related functions -- function isPointInPoly(poly, pt) { for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) && (c = !c); return c; } function findNearbySlice(mouseX, mouseY) { var slices = plot.getData(), options = plot.getOptions(), radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius, x, y; for (var i = 0; i < slices.length; ++i) { var s = slices[i]; if (s.pie.show) { ctx.save(); ctx.beginPath(); ctx.moveTo(0, 0); // Center of the pie //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); ctx.closePath(); x = mouseX - centerLeft; y = mouseY - centerTop; if (ctx.isPointInPath) { if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { ctx.restore(); return { datapoint: [s.percent, s.data], dataIndex: 0, series: s, seriesIndex: i }; } } else { // excanvas for IE doesn;t support isPointInPath, this is a workaround. var p1X = radius * Math.cos(s.startAngle), p1Y = radius * Math.sin(s.startAngle), p2X = radius * Math.cos(s.startAngle + s.angle / 4), p2Y = radius * Math.sin(s.startAngle + s.angle / 4), p3X = radius * Math.cos(s.startAngle + s.angle / 2), p3Y = radius * Math.sin(s.startAngle + s.angle / 2), p4X = radius * Math.cos(s.startAngle + s.angle / 1.5), p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5), p5X = radius * Math.cos(s.startAngle + s.angle), p5Y = radius * Math.sin(s.startAngle + s.angle), arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]], arrPoint = [x, y]; // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt? if (isPointInPoly(arrPoly, arrPoint)) { ctx.restore(); return { datapoint: [s.percent, s.data], dataIndex: 0, series: s, seriesIndex: i }; } } ctx.restore(); } } return null; } function onMouseMove(e) { triggerClickHoverEvent("plothover", e); } function onClick(e) { triggerClickHoverEvent("plotclick", e); } // trigger click or hover event (they send the same parameters so we share their code) function triggerClickHoverEvent(eventname, e) { var offset = plot.offset(); var canvasX = parseInt(e.pageX - offset.left); var canvasY = parseInt(e.pageY - offset.top); var item = findNearbySlice(canvasX, canvasY); if (options.grid.autoHighlight) { // clear auto-highlights for (var i = 0; i < highlights.length; ++i) { var h = highlights[i]; if (h.auto == eventname && !(item && h.series == item.series)) { unhighlight(h.series); } } } // highlight the slice if (item) { highlight(item.series, eventname); } // trigger any hover bind events var pos = { pageX: e.pageX, pageY: e.pageY }; target.trigger(eventname, [pos, item]); } function highlight(s, auto) { //if (typeof s == "number") { // s = series[s]; //} var i = indexOfHighlight(s); if (i == -1) { highlights.push({ series: s, auto: auto }); plot.triggerRedrawOverlay(); } else if (!auto) { highlights[i].auto = false; } } function unhighlight(s) { if (s == null) { highlights = []; plot.triggerRedrawOverlay(); } //if (typeof s == "number") { // s = series[s]; //} var i = indexOfHighlight(s); if (i != -1) { highlights.splice(i, 1); plot.triggerRedrawOverlay(); } } function indexOfHighlight(s) { for (var i = 0; i < highlights.length; ++i) { var h = highlights[i]; if (h.series == s) return i; } return -1; } function drawOverlay(plot, octx) { var options = plot.getOptions(); var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; octx.save(); octx.translate(centerLeft, centerTop); octx.scale(1, options.series.pie.tilt); for (var i = 0; i < highlights.length; ++i) { drawHighlight(highlights[i].series); } drawDonutHole(octx); octx.restore(); function drawHighlight(series) { if (series.angle <= 0 || isNaN(series.angle)) { return; } //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor octx.beginPath(); if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { octx.moveTo(0, 0); // Center of the pie } octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false); octx.closePath(); octx.fill(); } } } // end init (plugin body) // define pie specific options and their default values var options = { series: { pie: { show: false, radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) innerRadius: 0, /* for donut */ startAngle: 3/2, tilt: 1, shadow: { left: 5, // shadow left offset top: 15, // shadow top offset alpha: 0.02 // shadow alpha }, offset: { top: 0, left: "auto" }, stroke: { color: "#fff", width: 1 }, label: { show: "auto", formatter: function(label, slice) { return "<div style='font-size:x-small;text-align:center;padding:2px;color:" + slice.color + ";'>" + label + "<br/>" + Math.round(slice.percent) + "%</div>"; }, // formatter function radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) background: { color: null, opacity: 0 }, threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) }, combine: { threshold: -1, // percentage at which to combine little slices into one larger slice color: null, // color to give the new slice (auto-generated if null) label: "Other" // label to give the new slice }, highlight: { //color: "#fff", // will add this functionality once parseColor is available opacity: 0.5 } } } }; $.plot.plugins.push({ init: init, options: options, name: "pie", version: "1.1" }); })(jQuery); justgage/justgage-demo.js 0000644 00000002647 15030376016 0011460 0 ustar 00 /* justGage charts */ $(function() { "use strict"; var g1, g2, g3, g4, g5; window.onload = function() { var g1 = new JustGage({ id: "g1", value: getRandomInt(0, 100), min: 0, max: 100, title: "Big Fella", label: "pounds" }); var g2 = new JustGage({ id: "g2", value: getRandomInt(0, 100), min: 0, max: 100, title: "Small Buddy", label: "oz" }); var g3 = new JustGage({ id: "g3", value: getRandomInt(0, 100), min: 0, max: 100, title: "Tiny Lad", label: "oz" }); var g4 = new JustGage({ id: "g4", value: getRandomInt(0, 100), min: 0, max: 100, title: "Little Pal", label: "oz" }); var g5 = new JustGage({ id: "g5", value: getRandomInt(0, 100), min: 0, max: 100, title: "Little Pal", label: "oz" }); setInterval(function() { g1.refresh(getRandomInt(50, 100)); g2.refresh(getRandomInt(50, 100)); g3.refresh(getRandomInt(0, 50)); g4.refresh(getRandomInt(0, 50)); g5.refresh(getRandomInt(0, 50)); }, 2500); }; }); justgage/justgage.js 0000644 00000112337 15030376016 0010534 0 ustar 00 /** * JustGage - animated gauges using RaphaelJS * Check http://www.justgage.com for official releases * Licensed under MIT. * @author Bojan Djuricic (@Toorshia) **/ JustGage = function(config) { var obj = this; // Helps in case developer wants to debug it. unobtrusive if (config === null || config === undefined) { console.log('* justgage: Make sure to pass options to the constructor!'); return false; } var node; if (config.id !== null && config.id !== undefined) { node = document.getElementById(config.id); if (!node) { console.log('* justgage: No element with id : %s found', config.id); return false; } } else if (config.parentNode !== null && config.parentNode !== undefined) { node = config.parentNode; } else { console.log('* justgage: Make sure to pass the existing element id or parentNode to the constructor.'); return false; } var dataset = node.dataset ? node.dataset : {}; // check for defaults var defaults = (config.defaults !== null && config.defaults !== undefined) ? config.defaults : false; if (defaults !== false) { config = extend({}, config, defaults); delete config.defaults; } // configurable parameters obj.config = { // id : string // this is container element id id: config.id, // value : float // value gauge is showing value: kvLookup('value', config, dataset, 0, 'float'), // defaults : bool // defaults parameter to use defaults: kvLookup('defaults', config, dataset, 0, false), // parentNode : node object // this is container element parentNode: kvLookup('parentNode', config, dataset, null), // width : int // gauge width width: kvLookup('width', config, dataset, null), // height : int // gauge height height: kvLookup('height', config, dataset, null), // title : string // gauge title title: kvLookup('title', config, dataset, ""), // titleFontColor : string // color of gauge title titleFontColor: kvLookup('titleFontColor', config, dataset, "#999999"), // titleFontFamily : string // color of gauge title titleFontFamily: kvLookup('titleFontFamily', config, dataset, "sans-serif"), // titlePosition : string // 'above' or 'below' titlePosition: kvLookup('titlePosition', config, dataset, "above"), // valueFontColor : string // color of label showing current value valueFontColor: kvLookup('valueFontColor', config, dataset, "#010101"), // valueFontFamily : string // color of label showing current value valueFontFamily: kvLookup('valueFontFamily', config, dataset, "Arial"), // symbol : string // special symbol to show next to value symbol: kvLookup('symbol', config, dataset, ''), // min : float // min value min: kvLookup('min', config, dataset, 0, 'float'), // max : float // max value max: kvLookup('max', config, dataset, 100, 'float'), // reverse : bool // reverse min and max reverse: kvLookup('reverse', config, dataset, false), // humanFriendlyDecimal : int // number of decimal places for our human friendly number to contain humanFriendlyDecimal: kvLookup('humanFriendlyDecimal', config, dataset, 0), // textRenderer: func // function applied before rendering text textRenderer: kvLookup('textRenderer', config, dataset, null), // gaugeWidthScale : float // width of the gauge element gaugeWidthScale: kvLookup('gaugeWidthScale', config, dataset, 1.0), // gaugeColor : string // background color of gauge element gaugeColor: kvLookup('gaugeColor', config, dataset, "#edebeb"), // label : string // text to show below value label: kvLookup('label', config, dataset, ''), // labelFontColor : string // color of label showing label under value labelFontColor: kvLookup('labelFontColor', config, dataset, "#b3b3b3"), // shadowOpacity : int // 0 ~ 1 shadowOpacity: kvLookup('shadowOpacity', config, dataset, 0.2), // shadowSize: int // inner shadow size shadowSize: kvLookup('shadowSize', config, dataset, 5), // shadowVerticalOffset : int // how much shadow is offset from top shadowVerticalOffset: kvLookup('shadowVerticalOffset', config, dataset, 3), // levelColors : string[] // colors of indicator, from lower to upper, in RGB format levelColors: kvLookup('levelColors', config, dataset, ["#a9d70b", "#f9c802", "#ff0000"], 'array', ','), // startAnimationTime : int // length of initial animation startAnimationTime: kvLookup('startAnimationTime', config, dataset, 700), // startAnimationType : string // type of initial animation (linear, >, <, <>, bounce) startAnimationType: kvLookup('startAnimationType', config, dataset, '>'), // refreshAnimationTime : int // length of refresh animation refreshAnimationTime: kvLookup('refreshAnimationTime', config, dataset, 700), // refreshAnimationType : string // type of refresh animation (linear, >, <, <>, bounce) refreshAnimationType: kvLookup('refreshAnimationType', config, dataset, '>'), // donutStartAngle : int // angle to start from when in donut mode donutStartAngle: kvLookup('donutStartAngle', config, dataset, 90), // valueMinFontSize : int // absolute minimum font size for the value valueMinFontSize: kvLookup('valueMinFontSize', config, dataset, 16), // titleMinFontSize // absolute minimum font size for the title titleMinFontSize: kvLookup('titleMinFontSize', config, dataset, 10), // labelMinFontSize // absolute minimum font size for the label labelMinFontSize: kvLookup('labelMinFontSize', config, dataset, 10), // minLabelMinFontSize // absolute minimum font size for the minimum label minLabelMinFontSize: kvLookup('minLabelMinFontSize', config, dataset, 10), // maxLabelMinFontSize // absolute minimum font size for the maximum label maxLabelMinFontSize: kvLookup('maxLabelMinFontSize', config, dataset, 10), // hideValue : bool // hide value text hideValue: kvLookup('hideValue', config, dataset, false), // hideMinMax : bool // hide min and max values hideMinMax: kvLookup('hideMinMax', config, dataset, false), // hideInnerShadow : bool // hide inner shadow hideInnerShadow: kvLookup('hideInnerShadow', config, dataset, false), // humanFriendly : bool // convert large numbers for min, max, value to human friendly (e.g. 1234567 -> 1.23M) humanFriendly: kvLookup('humanFriendly', config, dataset, false), // noGradient : bool // whether to use gradual color change for value, or sector-based noGradient: kvLookup('noGradient', config, dataset, false), // donut : bool // show full donut gauge donut: kvLookup('donut', config, dataset, false), // relativeGaugeSize : bool // whether gauge size should follow changes in container element size relativeGaugeSize: kvLookup('relativeGaugeSize', config, dataset, false), // counter : bool // animate level number change counter: kvLookup('counter', config, dataset, false), // decimals : int // number of digits after floating point decimals: kvLookup('decimals', config, dataset, 0), // customSectors : [] of objects // number of digits after floating point customSectors: kvLookup('customSectors', config, dataset, []), // formatNumber: boolean // formats numbers with commas where appropriate formatNumber: kvLookup('formatNumber', config, dataset, false), // pointer : bool // show value pointer pointer: kvLookup('pointer', config, dataset, false), // pointerOptions : object // define pointer look pointerOptions: kvLookup('pointerOptions', config, dataset, []) }; // variables var canvasW, canvasH, widgetW, widgetH, aspect, dx, dy, titleFontSize, titleX, titleY, valueFontSize, valueX, valueY, labelFontSize, labelX, labelY, minFontSize, minX, minY, maxFontSize, maxX, maxY; // overflow values if (obj.config.value > obj.config.max) obj.config.value = obj.config.max; if (obj.config.value < obj.config.min) obj.config.value = obj.config.min; obj.originalValue = kvLookup('value', config, dataset, -1, 'float'); // create canvas if (obj.config.id !== null && (document.getElementById(obj.config.id)) !== null) { obj.canvas = Raphael(obj.config.id, "100%", "100%"); } else if (obj.config.parentNode !== null) { obj.canvas = Raphael(obj.config.parentNode, "100%", "100%"); } if (obj.config.relativeGaugeSize === true) { obj.canvas.setViewBox(0, 0, 200, 150, true); } // canvas dimensions if (obj.config.relativeGaugeSize === true) { canvasW = 200; canvasH = 150; } else if (obj.config.width !== null && obj.config.height !== null) { canvasW = obj.config.width; canvasH = obj.config.height; } else if (obj.config.parentNode !== null) { obj.canvas.setViewBox(0, 0, 200, 150, true); canvasW = 200; canvasH = 150; } else { canvasW = getStyle(document.getElementById(obj.config.id), "width").slice(0, -2) * 1; canvasH = getStyle(document.getElementById(obj.config.id), "height").slice(0, -2) * 1; } // widget dimensions if (obj.config.donut === true) { // DONUT ******************************* // width more than height if (canvasW > canvasH) { widgetH = canvasH; widgetW = widgetH; // width less than height } else if (canvasW < canvasH) { widgetW = canvasW; widgetH = widgetW; // if height don't fit, rescale both if (widgetH > canvasH) { aspect = widgetH / canvasH; widgetH = widgetH / aspect; widgetW = widgetH / aspect; } // equal } else { widgetW = canvasW; widgetH = widgetW; } // delta dx = (canvasW - widgetW) / 2; dy = (canvasH - widgetH) / 2; // title titleFontSize = ((widgetH / 8) > 10) ? (widgetH / 10) : 10; titleX = dx + widgetW / 2; titleY = dy + widgetH / 11; // value valueFontSize = ((widgetH / 6.4) > 16) ? (widgetH / 5.4) : 18; valueX = dx + widgetW / 2; if (obj.config.label !== '') { valueY = dy + widgetH / 1.85; } else { valueY = dy + widgetH / 1.7; } // label labelFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10; labelX = dx + widgetW / 2; labelY = valueY + labelFontSize; // min minFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10; minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2; minY = labelY; // max maxFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10; maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2; maxY = labelY; } else { // HALF ******************************* // width more than height if (canvasW > canvasH) { widgetH = canvasH; widgetW = widgetH * 1.25; //if width doesn't fit, rescale both if (widgetW > canvasW) { aspect = widgetW / canvasW; widgetW = widgetW / aspect; widgetH = widgetH / aspect; } // width less than height } else if (canvasW < canvasH) { widgetW = canvasW; widgetH = widgetW / 1.25; // if height don't fit, rescale both if (widgetH > canvasH) { aspect = widgetH / canvasH; widgetH = widgetH / aspect; widgetW = widgetH / aspect; } // equal } else { widgetW = canvasW; widgetH = widgetW * 0.75; } // delta dx = (canvasW - widgetW) / 2; dy = (canvasH - widgetH) / 2; if (obj.config.titlePosition === 'below') { // shift whole thing down dy -= (widgetH / 6.4); } // title titleFontSize = ((widgetH / 8) > obj.config.titleMinFontSize) ? (widgetH / 10) : obj.config.titleMinFontSize; titleX = dx + widgetW / 2; titleY = dy + (obj.config.titlePosition === 'below' ? (widgetH * 1.07) : (widgetH / 6.4)); // value valueFontSize = ((widgetH / 6.5) > obj.config.valueMinFontSize) ? (widgetH / 6.5) : obj.config.valueMinFontSize; valueX = dx + widgetW / 2; valueY = dy + widgetH / 1.275; // label labelFontSize = ((widgetH / 16) > obj.config.labelMinFontSize) ? (widgetH / 16) : obj.config.labelMinFontSize; labelX = dx + widgetW / 2; labelY = valueY + valueFontSize / 2 + 5; // min minFontSize = ((widgetH / 16) > obj.config.minLabelMinFontSize) ? (widgetH / 16) : obj.config.minLabelMinFontSize; minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2; minY = labelY; // max maxFontSize = ((widgetH / 16) > obj.config.maxLabelMinFontSize) ? (widgetH / 16) : obj.config.maxLabelMinFontSize; maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2; maxY = labelY; } // parameters obj.params = { canvasW: canvasW, canvasH: canvasH, widgetW: widgetW, widgetH: widgetH, dx: dx, dy: dy, titleFontSize: titleFontSize, titleX: titleX, titleY: titleY, valueFontSize: valueFontSize, valueX: valueX, valueY: valueY, labelFontSize: labelFontSize, labelX: labelX, labelY: labelY, minFontSize: minFontSize, minX: minX, minY: minY, maxFontSize: maxFontSize, maxX: maxX, maxY: maxY }; // var clear canvasW, canvasH, widgetW, widgetH, aspect, dx, dy, titleFontSize, titleX, titleY, valueFontSize, valueX, valueY, labelFontSize, labelX, labelY, minFontSize, minX, minY, maxFontSize, maxX, maxY = null; // pki - custom attribute for generating gauge paths obj.canvas.customAttributes.pki = function(value, min, max, w, h, dx, dy, gws, donut, reverse) { var alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path; if (donut) { alpha = (1 - 2 * (value - min) / (max - min)) * Math.PI; Ro = w / 2 - w / 7; Ri = Ro - w / 6.666666666666667 * gws; Cx = w / 2 + dx; Cy = h / 1.95 + dy; Xo = w / 2 + dx + Ro * Math.cos(alpha); Yo = h - (h - Cy) - Ro * Math.sin(alpha); Xi = w / 2 + dx + Ri * Math.cos(alpha); Yi = h - (h - Cy) - Ri * Math.sin(alpha); path = "M" + (Cx - Ri) + "," + Cy + " "; path += "L" + (Cx - Ro) + "," + Cy + " "; if (value > ((max - min) / 2)) { path += "A" + Ro + "," + Ro + " 0 0 1 " + (Cx + Ro) + "," + Cy + " "; } path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " "; path += "L" + Xi + "," + Yi + " "; if (value > ((max - min) / 2)) { path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx + Ri) + "," + Cy + " "; } path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " "; path += "Z "; return { path: path }; } else { alpha = (1 - (value - min) / (max - min)) * Math.PI; Ro = w / 2 - w / 10; Ri = Ro - w / 6.666666666666667 * gws; Cx = w / 2 + dx; Cy = h / 1.25 + dy; Xo = w / 2 + dx + Ro * Math.cos(alpha); Yo = h - (h - Cy) - Ro * Math.sin(alpha); Xi = w / 2 + dx + Ri * Math.cos(alpha); Yi = h - (h - Cy) - Ri * Math.sin(alpha); path = "M" + (Cx - Ri) + "," + Cy + " "; path += "L" + (Cx - Ro) + "," + Cy + " "; path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " "; path += "L" + Xi + "," + Yi + " "; path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " "; path += "Z "; return { path: path }; } // var clear alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path = null; }; // ndl - custom attribute for generating needle path obj.canvas.customAttributes.ndl = function(value, min, max, w, h, dx, dy, gws, donut) { var dlt = w * 3.5 / 100; var dlb = w / 15; var dw = w / 100; if (obj.config.pointerOptions.toplength != null && obj.config.pointerOptions.toplength != undefined) dlt = obj.config.pointerOptions.toplength; if (obj.config.pointerOptions.bottomlength != null && obj.config.pointerOptions.bottomlength != undefined) dlb = obj.config.pointerOptions.bottomlength; if (obj.config.pointerOptions.bottomwidth != null && obj.config.pointerOptions.bottomwidth != undefined) dw = obj.config.pointerOptions.bottomwidth; var alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, Xc, Yc, Xz, Yz, Xa, Ya, Xb, Yb, path; if (donut) { alpha = (1 - 2 * (value - min) / (max - min)) * Math.PI; Ro = w / 2 - w / 7; Ri = Ro - w / 6.666666666666667 * gws; Cx = w / 2 + dx; Cy = h / 1.95 + dy; Xo = w / 2 + dx + Ro * Math.cos(alpha); Yo = h - (h - Cy) - Ro * Math.sin(alpha); Xi = w / 2 + dx + Ri * Math.cos(alpha); Yi = h - (h - Cy) - Ri * Math.sin(alpha); Xc = Xo + dlt * Math.cos(alpha); Yc = Yo - dlt * Math.sin(alpha); Xz = Xi - dlb * Math.cos(alpha); Yz = Yi + dlb * Math.sin(alpha); Xa = Xz + dw * Math.sin(alpha); Ya = Yz + dw * Math.cos(alpha); Xb = Xz - dw * Math.sin(alpha); Yb = Yz - dw * Math.cos(alpha); path = 'M' + Xa + ',' + Ya + ' '; path += 'L' + Xb + ',' + Yb + ' '; path += 'L' + Xc + ',' + Yc + ' '; path += 'Z '; return { path: path }; } else { alpha = (1 - (value - min) / (max - min)) * Math.PI; Ro = w / 2 - w / 10; Ri = Ro - w / 6.666666666666667 * gws; Cx = w / 2 + dx; Cy = h / 1.25 + dy; Xo = w / 2 + dx + Ro * Math.cos(alpha); Yo = h - (h - Cy) - Ro * Math.sin(alpha); Xi = w / 2 + dx + Ri * Math.cos(alpha); Yi = h - (h - Cy) - Ri * Math.sin(alpha); Xc = Xo + dlt * Math.cos(alpha); Yc = Yo - dlt * Math.sin(alpha); Xz = Xi - dlb * Math.cos(alpha); Yz = Yi + dlb * Math.sin(alpha); Xa = Xz + dw * Math.sin(alpha); Ya = Yz + dw * Math.cos(alpha); Xb = Xz - dw * Math.sin(alpha); Yb = Yz - dw * Math.cos(alpha); path = 'M' + Xa + ',' + Ya + ' '; path += 'L' + Xb + ',' + Yb + ' '; path += 'L' + Xc + ',' + Yc + ' '; path += 'Z '; return { path: path }; } // var clear alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, Xc, Yc, Xz, Yz, Xa, Ya, Xb, Yb, path = null; }; // gauge obj.gauge = obj.canvas.path().attr({ "stroke": "none", "fill": obj.config.gaugeColor, pki: [ obj.config.max, obj.config.min, obj.config.max, obj.params.widgetW, obj.params.widgetH, obj.params.dx, obj.params.dy, obj.config.gaugeWidthScale, obj.config.donut, obj.config.reverse ] }); // level obj.level = obj.canvas.path().attr({ "stroke": "none", "fill": getColor(obj.config.value, (obj.config.value - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors), pki: [ obj.config.min, obj.config.min, obj.config.max, obj.params.widgetW, obj.params.widgetH, obj.params.dx, obj.params.dy, obj.config.gaugeWidthScale, obj.config.donut, obj.config.reverse ] }); if (obj.config.donut) { obj.level.transform("r" + obj.config.donutStartAngle + ", " + (obj.params.widgetW / 2 + obj.params.dx) + ", " + (obj.params.widgetH / 1.95 + obj.params.dy)); } if (obj.config.pointer) { // needle obj.needle = obj.canvas.path().attr({ "stroke": (obj.config.pointerOptions.stroke !== null && obj.config.pointerOptions.stroke !== undefined) ? obj.config.pointerOptions.stroke : "none", "stroke-width": (obj.config.pointerOptions.stroke_width !== null && obj.config.pointerOptions.stroke_width !== undefined) ? obj.config.pointerOptions.stroke_width : 0, "stroke-linecap": (obj.config.pointerOptions.stroke_linecap !== null && obj.config.pointerOptions.stroke_linecap !== undefined) ? obj.config.pointerOptions.stroke_linecap : "square", "fill": (obj.config.pointerOptions.color !== null && obj.config.pointerOptions.color !== undefined) ? obj.config.pointerOptions.color : "#000000", ndl: [ obj.config.min, obj.config.min, obj.config.max, obj.params.widgetW, obj.params.widgetH, obj.params.dx, obj.params.dy, obj.config.gaugeWidthScale, obj.config.donut ] }); if (obj.config.donut) { obj.needle.transform("r" + obj.config.donutStartAngle + ", " + (obj.params.widgetW / 2 + obj.params.dx) + ", " + (obj.params.widgetH / 1.95 + obj.params.dy)); } } // title obj.txtTitle = obj.canvas.text(obj.params.titleX, obj.params.titleY, obj.config.title); obj.txtTitle.attr({ "font-size": obj.params.titleFontSize, "font-weight": "bold", "font-family": obj.config.titleFontFamily, "fill": obj.config.titleFontColor, "fill-opacity": "1" }); setDy(obj.txtTitle, obj.params.titleFontSize, obj.params.titleY); // value obj.txtValue = obj.canvas.text(obj.params.valueX, obj.params.valueY, 0); obj.txtValue.attr({ "font-size": obj.params.valueFontSize, "font-weight": "bold", "font-family": obj.config.valueFontFamily, "fill": obj.config.valueFontColor, "fill-opacity": "0" }); setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY); // label obj.txtLabel = obj.canvas.text(obj.params.labelX, obj.params.labelY, obj.config.label); obj.txtLabel.attr({ "font-size": obj.params.labelFontSize, "font-weight": "normal", "font-family": "Arial", "fill": obj.config.labelFontColor, "fill-opacity": "0" }); setDy(obj.txtLabel, obj.params.labelFontSize, obj.params.labelY); // min var min = obj.config.min; if (obj.config.reverse) { min = obj.config.max; } obj.txtMinimum = min; if (obj.config.humanFriendly) { obj.txtMinimum = humanFriendlyNumber(min, obj.config.humanFriendlyDecimal); } else if (obj.config.formatNumber) { obj.txtMinimum = formatNumber(min); } obj.txtMin = obj.canvas.text(obj.params.minX, obj.params.minY, obj.txtMinimum); obj.txtMin.attr({ "font-size": obj.params.minFontSize, "font-weight": "normal", "font-family": "Arial", "fill": obj.config.labelFontColor, "fill-opacity": (obj.config.hideMinMax || obj.config.donut) ? "0" : "1" }); setDy(obj.txtMin, obj.params.minFontSize, obj.params.minY); // max var max = obj.config.max; if (obj.config.reverse) { max = obj.config.min; } obj.txtMaximum = max; if (obj.config.humanFriendly) { obj.txtMaximum = humanFriendlyNumber(max, obj.config.humanFriendlyDecimal); } else if (obj.config.formatNumber) { obj.txtMaximum = formatNumber(max); } obj.txtMax = obj.canvas.text(obj.params.maxX, obj.params.maxY, obj.txtMaximum); obj.txtMax.attr({ "font-size": obj.params.maxFontSize, "font-weight": "normal", "font-family": "Arial", "fill": obj.config.labelFontColor, "fill-opacity": (obj.config.hideMinMax || obj.config.donut) ? "0" : "1" }); setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY); var defs = obj.canvas.canvas.childNodes[1]; var svg = "http://www.w3.org/2000/svg"; if (ie !== 'undefined' && ie < 9) { // VML mode - no SVG & SVG filter support } else if (ie !== 'undefined') { onCreateElementNsReady(function() { obj.generateShadow(svg, defs); }); } else { obj.generateShadow(svg, defs); } // var clear defs, svg = null; // set value to display if (obj.config.textRenderer) { obj.originalValue = obj.config.textRenderer(obj.originalValue); } else if (obj.config.humanFriendly) { obj.originalValue = humanFriendlyNumber(obj.originalValue, obj.config.humanFriendlyDecimal) + obj.config.symbol; } else if (obj.config.formatNumber) { obj.originalValue = formatNumber(obj.originalValue) + obj.config.symbol; } else { obj.originalValue = (obj.originalValue * 1).toFixed(obj.config.decimals) + obj.config.symbol; } if (obj.config.counter === true) { //on each animation frame eve.on("raphael.anim.frame." + (obj.level.id), function() { var currentValue = obj.level.attr("pki")[0]; if (obj.config.reverse) { currentValue = (obj.config.max * 1) + (obj.config.min * 1) - (obj.level.attr("pki")[0] * 1); } if (obj.config.textRenderer) { obj.txtValue.attr("text", obj.config.textRenderer(Math.floor(currentValue))); } else if (obj.config.humanFriendly) { obj.txtValue.attr("text", humanFriendlyNumber(Math.floor(currentValue), obj.config.humanFriendlyDecimal) + obj.config.symbol); } else if (obj.config.formatNumber) { obj.txtValue.attr("text", formatNumber(Math.floor(currentValue)) + obj.config.symbol); } else { obj.txtValue.attr("text", (currentValue * 1).toFixed(obj.config.decimals) + obj.config.symbol); } setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY); currentValue = null; }); //on animation end eve.on("raphael.anim.finish." + (obj.level.id), function() { obj.txtValue.attr({ "text": obj.originalValue }); setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY); }); } else { //on animation start eve.on("raphael.anim.start." + (obj.level.id), function() { obj.txtValue.attr({ "text": obj.originalValue }); setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY); }); } // animate gauge level, value & label var rvl = obj.config.value; if (obj.config.reverse) { rvl = (obj.config.max * 1) + (obj.config.min * 1) - (obj.config.value * 1); } obj.level.animate({ pki: [ rvl, obj.config.min, obj.config.max, obj.params.widgetW, obj.params.widgetH, obj.params.dx, obj.params.dy, obj.config.gaugeWidthScale, obj.config.donut, obj.config.reverse ] }, obj.config.startAnimationTime, obj.config.startAnimationType); if (obj.config.pointer) { obj.needle.animate({ ndl: [ rvl, obj.config.min, obj.config.max, obj.params.widgetW, obj.params.widgetH, obj.params.dx, obj.params.dy, obj.config.gaugeWidthScale, obj.config.donut ] }, obj.config.startAnimationTime, obj.config.startAnimationType); } obj.txtValue.animate({ "fill-opacity": (obj.config.hideValue) ? "0" : "1" }, obj.config.startAnimationTime, obj.config.startAnimationType); obj.txtLabel.animate({ "fill-opacity": "1" }, obj.config.startAnimationTime, obj.config.startAnimationType); }; /** Refresh gauge level */ JustGage.prototype.refresh = function(val, max) { var obj = this; var displayVal, color, max = max || null; // set new max if (max !== null) { obj.config.max = max; // TODO: update customSectors obj.txtMaximum = obj.config.max; if (obj.config.humanFriendly) { obj.txtMaximum = humanFriendlyNumber(obj.config.max, obj.config.humanFriendlyDecimal); } else if (obj.config.formatNumber) { obj.txtMaximum = formatNumber(obj.config.max); } if (!obj.config.reverse) { obj.txtMax.attr({ "text": obj.txtMaximum }); setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY); } else { obj.txtMin.attr({ "text": obj.txtMaximum }); setDy(obj.txtMin, obj.params.minFontSize, obj.params.minY); } } // overflow values displayVal = val; if ((val * 1) > (obj.config.max * 1)) { val = (obj.config.max * 1); } if ((val * 1) < (obj.config.min * 1)) { val = (obj.config.min * 1); } color = getColor(val, (val - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors); if (obj.config.textRenderer) { displayVal = obj.config.textRenderer(displayVal); } else if (obj.config.humanFriendly) { displayVal = humanFriendlyNumber(displayVal, obj.config.humanFriendlyDecimal) + obj.config.symbol; } else if (obj.config.formatNumber) { displayVal = formatNumber((displayVal * 1).toFixed(obj.config.decimals)) + obj.config.symbol; } else { displayVal = (displayVal * 1).toFixed(obj.config.decimals) + obj.config.symbol; } obj.originalValue = displayVal; obj.config.value = val * 1; if (!obj.config.counter) { obj.txtValue.attr({ "text": displayVal }); setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY); } var rvl = obj.config.value; if (obj.config.reverse) { rvl = (obj.config.max * 1) + (obj.config.min * 1) - (obj.config.value * 1); } obj.level.animate({ pki: [ rvl, obj.config.min, obj.config.max, obj.params.widgetW, obj.params.widgetH, obj.params.dx, obj.params.dy, obj.config.gaugeWidthScale, obj.config.donut, obj.config.reverse ], "fill": color }, obj.config.refreshAnimationTime, obj.config.refreshAnimationType); if (obj.config.pointer) { obj.needle.animate({ ndl: [ rvl, obj.config.min, obj.config.max, obj.params.widgetW, obj.params.widgetH, obj.params.dx, obj.params.dy, obj.config.gaugeWidthScale, obj.config.donut ] }, obj.config.refreshAnimationTime, obj.config.refreshAnimationType); } // var clear obj, displayVal, color, max = null; }; /** Generate shadow */ JustGage.prototype.generateShadow = function(svg, defs) { var obj = this; var sid = "inner-shadow-" + obj.config.id; var gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3; // FILTER gaussFilter = document.createElementNS(svg, "filter"); gaussFilter.setAttribute("id", sid); defs.appendChild(gaussFilter); // offset feOffset = document.createElementNS(svg, "feOffset"); feOffset.setAttribute("dx", 0); feOffset.setAttribute("dy", obj.config.shadowVerticalOffset); gaussFilter.appendChild(feOffset); // blur feGaussianBlur = document.createElementNS(svg, "feGaussianBlur"); feGaussianBlur.setAttribute("result", "offset-blur"); feGaussianBlur.setAttribute("stdDeviation", obj.config.shadowSize); gaussFilter.appendChild(feGaussianBlur); // composite 1 feComposite1 = document.createElementNS(svg, "feComposite"); feComposite1.setAttribute("operator", "out"); feComposite1.setAttribute("in", "SourceGraphic"); feComposite1.setAttribute("in2", "offset-blur"); feComposite1.setAttribute("result", "inverse"); gaussFilter.appendChild(feComposite1); // flood feFlood = document.createElementNS(svg, "feFlood"); feFlood.setAttribute("flood-color", "black"); feFlood.setAttribute("flood-opacity", obj.config.shadowOpacity); feFlood.setAttribute("result", "color"); gaussFilter.appendChild(feFlood); // composite 2 feComposite2 = document.createElementNS(svg, "feComposite"); feComposite2.setAttribute("operator", "in"); feComposite2.setAttribute("in", "color"); feComposite2.setAttribute("in2", "inverse"); feComposite2.setAttribute("result", "shadow"); gaussFilter.appendChild(feComposite2); // composite 3 feComposite3 = document.createElementNS(svg, "feComposite"); feComposite3.setAttribute("operator", "over"); feComposite3.setAttribute("in", "shadow"); feComposite3.setAttribute("in2", "SourceGraphic"); gaussFilter.appendChild(feComposite3); // set shadow if (!obj.config.hideInnerShadow) { obj.canvas.canvas.childNodes[2].setAttribute("filter", "url(#" + sid + ")"); obj.canvas.canvas.childNodes[3].setAttribute("filter", "url(#" + sid + ")"); } // var clear gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3 = null; }; // // tiny helper function to lookup value of a key from two hash tables // if none found, return defaultvalue // // key: string // tablea: object // tableb: DOMStringMap|object // defval: string|integer|float|null // datatype: return datatype // delimiter: delimiter to be used in conjunction with datatype formatting // function kvLookup(key, tablea, tableb, defval, datatype, delimiter) { var val = defval; var canConvert = false; if (!(key === null || key === undefined)) { if (tableb !== null && tableb !== undefined && typeof tableb === "object" && key in tableb) { val = tableb[key]; canConvert = true; } else if (tablea !== null && tablea !== undefined && typeof tablea === "object" && key in tablea) { val = tablea[key]; canConvert = true; } else { val = defval; } if (canConvert === true) { if (datatype !== null && datatype !== undefined) { switch (datatype) { case 'int': val = parseInt(val, 10); break; case 'float': val = parseFloat(val); break; default: break; } } } } return val; }; /** Get color for value */ function getColor(val, pct, col, noGradient, custSec) { var no, inc, colors, percentage, rval, gval, bval, lower, upper, range, rangePct, pctLower, pctUpper, color; var noGradient = noGradient || custSec.length > 0; if (custSec.length > 0) { for (var i = 0; i < custSec.length; i++) { if (val > custSec[i].lo && val <= custSec[i].hi) { return custSec[i].color; } } } no = col.length; if (no === 1) return col[0]; inc = (noGradient) ? (1 / no) : (1 / (no - 1)); colors = []; for (i = 0; i < col.length; i++) { percentage = (noGradient) ? (inc * (i + 1)) : (inc * i); rval = parseInt((cutHex(col[i])).substring(0, 2), 16); gval = parseInt((cutHex(col[i])).substring(2, 4), 16); bval = parseInt((cutHex(col[i])).substring(4, 6), 16); colors[i] = { pct: percentage, color: { r: rval, g: gval, b: bval } }; } if (pct === 0) { return 'rgb(' + [colors[0].color.r, colors[0].color.g, colors[0].color.b].join(',') + ')'; } for (var j = 0; j < colors.length; j++) { if (pct <= colors[j].pct) { if (noGradient) { return 'rgb(' + [colors[j].color.r, colors[j].color.g, colors[j].color.b].join(',') + ')'; } else { lower = colors[j - 1]; upper = colors[j]; range = upper.pct - lower.pct; rangePct = (pct - lower.pct) / range; pctLower = 1 - rangePct; pctUpper = rangePct; color = { r: Math.floor(lower.color.r * pctLower + upper.color.r * pctUpper), g: Math.floor(lower.color.g * pctLower + upper.color.g * pctUpper), b: Math.floor(lower.color.b * pctLower + upper.color.b * pctUpper) }; return 'rgb(' + [color.r, color.g, color.b].join(',') + ')'; } } } } /** Fix Raphael display:none tspan dy attribute bug */ function setDy(elem, fontSize, txtYpos) { if ((!ie || ie > 9) && elem.node.firstChild.attributes.dy) { elem.node.firstChild.attributes.dy.value = 0; } } /** Random integer */ function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } /** Cut hex */ function cutHex(str) { return (str.charAt(0) == "#") ? str.substring(1, 7) : str; } /** Human friendly number suffix - From: http://stackoverflow.com/questions/2692323/code-golf-friendly-number-abbreviator */ function humanFriendlyNumber(n, d) { var p, d2, i, s; p = Math.pow; d2 = p(10, d); i = 7; while (i) { s = p(10, i-- * 3); if (s <= n) { n = Math.round(n * d2 / s) / d2 + "KMGTPE" [i]; } } return n; } /** Format numbers with commas - From: http://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript */ function formatNumber(x) { var parts = x.toString().split("."); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); return parts.join("."); } /** Get style */ function getStyle(oElm, strCssRule) { var strValue = ""; if (document.defaultView && document.defaultView.getComputedStyle) { strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule); } else if (oElm.currentStyle) { strCssRule = strCssRule.replace(/\-(\w)/g, function(strMatch, p1) { return p1.toUpperCase(); }); strValue = oElm.currentStyle[strCssRule]; } return strValue; } /** Create Element NS Ready */ function onCreateElementNsReady(func) { if (document.createElementNS !== undefined) { func(); } else { setTimeout(function() { onCreateElementNsReady(func); }, 100); } } /** Get IE version */ // ---------------------------------------------------------- // A short snippet for detecting versions of IE in JavaScript // without resorting to user-agent sniffing // ---------------------------------------------------------- // If you're not in IE (or IE version is less than 5) then: // ie === undefined // If you're in IE (>=5) then you can determine which version: // ie === 7; // IE7 // Thus, to detect IE: // if (ie) {} // And to detect the version: // ie === 6 // IE6 // ie > 7 // IE8, IE9 ... // ie < 9 // Anything less than IE9 // ---------------------------------------------------------- // UPDATE: Now using Live NodeList idea from @jdalton var ie = (function() { var undef, v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->', all[0] ); return v > 4 ? v : undef; }()); // extend target object with second object function extend(out) { out = out || {}; for (var i = 1; i < arguments.length; i++) { if (!arguments[i]) continue; for (var key in arguments[i]) { if (arguments[i].hasOwnProperty(key)) out[key] = arguments[i][key]; } } return out; }; justgage/justgage.css 0000644 00000000614 15030376016 0010702 0 ustar 00 /* justGage */ .xs-gauge { width: 90px; height: 70px; margin: 0 auto; } .sm-gauge { width: 130px; height: 100px; margin: 0 auto; } .md-gauge { width: 170px; height: 120px; margin: 0 auto; } .lg-gauge { width: 240px; height: 150px; margin: 0 auto; } .xl-gauge { width: 340px; height: 180px; margin: 0 auto; } xcharts/xcharts.js 0000644 00000140554 15030376016 0010244 0 ustar 00 /*! xCharts v0.3.0 Copyright (c) 2012, tenXer, Inc. All Rights Reserved. @license MIT license. http://github.com/tenXer/xcharts for details */ (function () { var xChart, _vis = {}, _scales = {}, _visutils = {}; (function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,v=e.reduce,h=e.reduceRight,g=e.filter,d=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,j=i.bind,w=function(n){return n instanceof w?n:this instanceof w?(this._wrapped=n,void 0):new w(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=w),exports._=w):n._=w,w.VERSION="1.4.3";var A=w.each=w.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(w.has(n,a)&&t.call(e,n[a],a,n)===r)return};w.map=w.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e[e.length]=t.call(r,n,u,i)}),e)};var O="Reduce of empty array with no initial value";w.reduce=w.foldl=w.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduce===v)return e&&(t=w.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(O);return r},w.reduceRight=w.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduceRight===h)return e&&(t=w.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=w.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(O);return r},w.find=w.detect=function(n,t,r){var e;return E(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},w.filter=w.select=function(n,t,r){var e=[];return null==n?e:g&&n.filter===g?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&(e[e.length]=n)}),e)},w.reject=function(n,t,r){return w.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},w.every=w.all=function(n,t,e){t||(t=w.identity);var u=!0;return null==n?u:d&&n.every===d?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var E=w.some=w.any=function(n,t,e){t||(t=w.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};w.contains=w.include=function(n,t){return null==n?!1:y&&n.indexOf===y?-1!=n.indexOf(t):E(n,function(n){return n===t})},w.invoke=function(n,t){var r=o.call(arguments,2);return w.map(n,function(n){return(w.isFunction(t)?t:n[t]).apply(n,r)})},w.pluck=function(n,t){return w.map(n,function(n){return n[t]})},w.where=function(n,t){return w.isEmpty(t)?[]:w.filter(n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},w.max=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.max.apply(Math,n);if(!t&&w.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>=e.computed&&(e={value:n,computed:a})}),e.value},w.min=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.min.apply(Math,n);if(!t&&w.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;e.computed>a&&(e={value:n,computed:a})}),e.value},w.shuffle=function(n){var t,r=0,e=[];return A(n,function(n){t=w.random(r++),e[r-1]=e[t],e[t]=n}),e};var F=function(n){return w.isFunction(n)?n:function(t){return t[n]}};w.sortBy=function(n,t,r){var e=F(t);return w.pluck(w.map(n,function(n,t,u){return{value:n,index:t,criteria:e.call(r,n,t,u)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||void 0===r)return 1;if(e>r||void 0===e)return-1}return n.index<t.index?-1:1}),"value")};var k=function(n,t,r,e){var u={},i=F(t||w.identity);return A(n,function(t,a){var o=i.call(r,t,a,n);e(u,o,t)}),u};w.groupBy=function(n,t,r){return k(n,t,r,function(n,t,r){(w.has(n,t)?n[t]:n[t]=[]).push(r)})},w.countBy=function(n,t,r){return k(n,t,r,function(n,t){w.has(n,t)||(n[t]=0),n[t]++})},w.sortedIndex=function(n,t,r,e){r=null==r?w.identity:F(r);for(var u=r.call(e,t),i=0,a=n.length;a>i;){var o=i+a>>>1;u>r.call(e,n[o])?i=o+1:a=o}return i},w.toArray=function(n){return n?w.isArray(n)?o.call(n):n.length===+n.length?w.map(n,w.identity):w.values(n):[]},w.size=function(n){return null==n?0:n.length===+n.length?n.length:w.keys(n).length},w.first=w.head=w.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:o.call(n,0,t)},w.initial=function(n,t,r){return o.call(n,0,n.length-(null==t||r?1:t))},w.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:o.call(n,Math.max(n.length-t,0))},w.rest=w.tail=w.drop=function(n,t,r){return o.call(n,null==t||r?1:t)},w.compact=function(n){return w.filter(n,w.identity)};var R=function(n,t,r){return A(n,function(n){w.isArray(n)?t?a.apply(r,n):R(n,t,r):r.push(n)}),r};w.flatten=function(n,t){return R(n,t,[])},w.without=function(n){return w.difference(n,o.call(arguments,1))},w.uniq=w.unique=function(n,t,r,e){w.isFunction(t)&&(e=r,r=t,t=!1);var u=r?w.map(n,r,e):n,i=[],a=[];return A(u,function(r,e){(t?e&&a[a.length-1]===r:w.contains(a,r))||(a.push(r),i.push(n[e]))}),i},w.union=function(){return w.uniq(c.apply(e,arguments))},w.intersection=function(n){var t=o.call(arguments,1);return w.filter(w.uniq(n),function(n){return w.every(t,function(t){return w.indexOf(t,n)>=0})})},w.difference=function(n){var t=c.apply(e,o.call(arguments,1));return w.filter(n,function(n){return!w.contains(t,n)})},w.zip=function(){for(var n=o.call(arguments),t=w.max(w.pluck(n,"length")),r=Array(t),e=0;t>e;e++)r[e]=w.pluck(n,""+e);return r},w.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},w.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=w.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},w.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},w.range=function(n,t,r){1>=arguments.length&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=Array(e);e>u;)i[u++]=n,n+=r;return i};var I=function(){};w.bind=function(n,t){var r,e;if(n.bind===j&&j)return j.apply(n,o.call(arguments,1));if(!w.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));I.prototype=n.prototype;var u=new I;I.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},w.bindAll=function(n){var t=o.call(arguments,1);return 0==t.length&&(t=w.functions(n)),A(t,function(t){n[t]=w.bind(n[t],n)}),n},w.memoize=function(n,t){var r={};return t||(t=w.identity),function(){var e=t.apply(this,arguments);return w.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},w.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},w.defer=function(n){return w.delay.apply(w,[n,1].concat(o.call(arguments,1)))},w.throttle=function(n,t){var r,e,u,i,a=0,o=function(){a=new Date,u=null,i=n.apply(r,e)};return function(){var c=new Date,l=t-(c-a);return r=this,e=arguments,0>=l?(clearTimeout(u),u=null,a=c,i=n.apply(r,e)):u||(u=setTimeout(o,l)),i}},w.debounce=function(n,t,r){var e,u;return function(){var i=this,a=arguments,o=function(){e=null,r||(u=n.apply(i,a))},c=r&&!e;return clearTimeout(e),e=setTimeout(o,t),c&&(u=n.apply(i,a)),u}},w.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},w.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},w.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},w.after=function(n,t){return 0>=n?t():function(){return 1>--n?t.apply(this,arguments):void 0}},w.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)w.has(n,r)&&(t[t.length]=r);return t},w.values=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push(n[r]);return t},w.pairs=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push([r,n[r]]);return t},w.invert=function(n){var t={};for(var r in n)w.has(n,r)&&(t[n[r]]=r);return t},w.functions=w.methods=function(n){var t=[];for(var r in n)w.isFunction(n[r])&&t.push(r);return t.sort()},w.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},w.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},w.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)w.contains(r,u)||(t[u]=n[u]);return t},w.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)null==n[r]&&(n[r]=t[r])}),n},w.clone=function(n){return w.isObject(n)?w.isArray(n)?n.slice():w.extend({},n):n},w.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof w&&(n=n._wrapped),t instanceof w&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==t+"";case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;r.push(n),e.push(t);var a=0,o=!0;if("[object Array]"==u){if(a=n.length,o=a==t.length)for(;a--&&(o=S(n[a],t[a],r,e)););}else{var c=n.constructor,f=t.constructor;if(c!==f&&!(w.isFunction(c)&&c instanceof c&&w.isFunction(f)&&f instanceof f))return!1;for(var s in n)if(w.has(n,s)&&(a++,!(o=w.has(t,s)&&S(n[s],t[s],r,e))))break;if(o){for(s in t)if(w.has(t,s)&&!a--)break;o=!a}}return r.pop(),e.pop(),o};w.isEqual=function(n,t){return S(n,t,[],[])},w.isEmpty=function(n){if(null==n)return!0;if(w.isArray(n)||w.isString(n))return 0===n.length;for(var t in n)if(w.has(n,t))return!1;return!0},w.isElement=function(n){return!(!n||1!==n.nodeType)},w.isArray=x||function(n){return"[object Array]"==l.call(n)},w.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){w["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),w.isArguments(arguments)||(w.isArguments=function(n){return!(!n||!w.has(n,"callee"))}),w.isFunction=function(n){return"function"==typeof n},w.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},w.isNaN=function(n){return w.isNumber(n)&&n!=+n},w.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},w.isNull=function(n){return null===n},w.isUndefined=function(n){return void 0===n},w.has=function(n,t){return f.call(n,t)},w.noConflict=function(){return n._=t,this},w.identity=function(n){return n},w.times=function(n,t,r){for(var e=Array(n),u=0;n>u;u++)e[u]=t.call(r,u);return e},w.random=function(n,t){return null==t&&(t=n,n=0),n+(0|Math.random()*(t-n+1))};var T={escape:{"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"}};T.unescape=w.invert(T.escape);var M={escape:RegExp("["+w.keys(T.escape).join("")+"]","g"),unescape:RegExp("("+w.keys(T.unescape).join("|")+")","g")};w.each(["escape","unescape"],function(n){w[n]=function(t){return null==t?"":(""+t).replace(M[n],function(t){return T[n][t]})}}),w.result=function(n,t){if(null==n)return null;var r=n[t];return w.isFunction(r)?r.call(n):r},w.mixin=function(n){A(w.functions(n),function(t){var r=w[t]=n[t];w.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),z.call(this,r.apply(w,n))}})};var N=0;w.uniqueId=function(n){var t=""+ ++N;return n?n+t:t},w.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\t|\u2028|\u2029/g;w.template=function(n,t,r){r=w.defaults({},r,w.templateSettings);var e=RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,a,o){return i+=n.slice(u,o).replace(D,function(n){return"\\"+B[n]}),r&&(i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(i+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),a&&(i+="';\n"+a+"\n__p+='"),u=o+t.length,t}),i+="';\n",r.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var a=Function(r.variable||"obj","_",i)}catch(o){throw o.source=i,o}if(t)return a(t,w);var c=function(n){return a.call(this,n,w)};return c.source="function("+(r.variable||"obj")+"){\n"+i+"}",c},w.chain=function(n){return w(n).chain()};var z=function(n){return this._chain?w(n).chain():n};w.mixin(w),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];w.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],z.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];w.prototype[n]=function(){return z.call(this,t.apply(this._wrapped,arguments))}}),w.extend(w.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this);function getInsertionPoint(zIndex) { return _.chain(_.range(zIndex, 10)).reverse().map(function (z) { return 'g[data-index="' + z + '"]'; }).value().join(', '); } function colorClass(el, i) { var c = el.getAttribute('class'); return ((c !== null) ? c.replace(/color\d+/g, '') : '') + ' color' + i; } _visutils = { getInsertionPoint: getInsertionPoint, colorClass: colorClass }; var local = this, defaultSpacing = 0.25; function _getDomain(data, axis) { return _.chain(data) .pluck('data') .flatten() .pluck(axis) .uniq() .filter(function (d) { return d !== undefined && d !== null; }) .value() .sort(d3.ascending); } _scales.ordinal = function (data, axis, bounds, extents) { var domain = _getDomain(data, axis); return d3.scale.ordinal() .domain(domain) .rangeRoundBands(bounds, defaultSpacing); }; _scales.linear = function (data, axis, bounds, extents) { return d3.scale.linear() .domain(extents) .nice() .rangeRound(bounds); }; _scales.exponential = function (data, axis, bounds, extents) { return d3.scale.pow() .exponent(0.65) .domain(extents) .nice() .rangeRound(bounds); }; _scales.time = function (data, axis, bounds, extents) { return d3.time.scale() .domain(_.map(extents, function (d) { return new Date(d); })) .range(bounds); }; function _extendDomain(domain, axis) { var min = domain[0], max = domain[1], diff, e; if (min === max) { e = Math.max(Math.round(min / 10), 4); min -= e; max += e; } diff = max - min; min = (min) ? min - (diff / 10) : min; min = (domain[0] > 0) ? Math.max(min, 0) : min; max = (max) ? max + (diff / 10) : max; max = (domain[1] < 0) ? Math.min(max, 0) : max; return [min, max]; } function _getExtents(options, data, xType, yType) { var extents, nData = _.chain(data) .pluck('data') .flatten() .value(); extents = { x: d3.extent(nData, function (d) { return d.x; }), y: d3.extent(nData, function (d) { return d.y; }) }; _.each([xType, yType], function (type, i) { var axis = (i) ? 'y' : 'x', extended; extents[axis] = d3.extent(nData, function (d) { return d[axis]; }); if (type === 'ordinal') { return; } _.each([axis + 'Min', axis + 'Max'], function (minMax, i) { if (type !== 'time') { extended = _extendDomain(extents[axis]); } if (options.hasOwnProperty(minMax) && options[minMax] !== null) { extents[axis][i] = options[minMax]; } else if (type !== 'time') { extents[axis][i] = extended[i]; } }); }); return extents; } _scales.xy = function (self, data, xType, yType) { var o = self._options, extents = _getExtents(o, data, xType, yType), scales = {}, horiz = [o.axisPaddingLeft, self._width], vert = [self._height, o.axisPaddingTop], xScale, yScale; _.each([xType, yType], function (type, i) { var axis = (i === 0) ? 'x' : 'y', bounds = (i === 0) ? horiz : vert, fn = xChart.getScale(type); scales[axis] = fn(data, axis, bounds, extents[axis]); }); return scales; }; (function () { var zIndex = 2, selector = 'g.bar', insertBefore = _visutils.getInsertionPoint(zIndex); function postUpdateScale(self, scaleData, mainData, compData) { self.xScale2 = d3.scale.ordinal() .domain(d3.range(0, mainData.length)) .rangeRoundBands([0, self.xScale.rangeBand()], 0.08); } function enter(self, storage, className, data, callbacks) { var barGroups, bars, yZero = self.yZero; barGroups = self._g.selectAll(selector + className) .data(data, function (d) { return d.className; }); barGroups.enter().insert('g', insertBefore) .attr('data-index', zIndex) .style('opacity', 0) .attr('class', function (d, i) { var cl = _.uniq((className + d.className).split('.')).join(' '); return cl + ' bar ' + _visutils.colorClass(this, i); }) .attr('transform', function (d, i) { return 'translate(' + self.xScale2(i) + ',0)'; }); bars = barGroups.selectAll('rect') .data(function (d) { return d.data; }, function (d) { return d.x; }); bars.enter().append('rect') .attr('width', 0) .attr('rx', 3) .attr('ry', 3) .attr('x', function (d) { return self.xScale(d.x) + (self.xScale2.rangeBand() / 2); }) .attr('height', function (d) { return Math.abs(yZero - self.yScale(d.y)); }) .attr('y', function (d) { return (d.y < 0) ? yZero : self.yScale(d.y); }) .on('mouseover', callbacks.mouseover) .on('mouseout', callbacks.mouseout) .on('click', callbacks.click); storage.barGroups = barGroups; storage.bars = bars; } function update(self, storage, timing) { var yZero = self.yZero; storage.barGroups .attr('class', function (d, i) { return _visutils.colorClass(this, i); }) .transition().duration(timing) .style('opacity', 1) .attr('transform', function (d, i) { return 'translate(' + self.xScale2(i) + ',0)'; }); storage.bars.transition().duration(timing) .attr('width', self.xScale2.rangeBand()) .attr('x', function (d) { return self.xScale(d.x); }) .attr('height', function (d) { return Math.abs(yZero - self.yScale(d.y)); }) .attr('y', function (d) { return (d.y < 0) ? yZero : self.yScale(d.y); }); } function exit(self, storage, timing) { storage.bars.exit() .transition().duration(timing) .attr('width', 0) .remove(); storage.barGroups.exit() .transition().duration(timing) .style('opacity', 0) .remove(); } function destroy(self, storage, timing) { var band = (self.xScale2) ? self.xScale2.rangeBand() / 2 : 0; delete self.xScale2; storage.bars .transition().duration(timing) .attr('width', 0) .attr('x', function (d) { return self.xScale(d.x) + band; }); } _vis.bar = { postUpdateScale: postUpdateScale, enter: enter, update: update, exit: exit, destroy: destroy }; }()); (function () { var zIndex = 3, selector = 'g.line', insertBefore = _visutils.getInsertionPoint(zIndex); function enter(self, storage, className, data, callbacks) { var inter = self._options.interpolation, x = function (d, i) { if (!self.xScale2 && !self.xScale.rangeBand) { return self.xScale(d.x); } return self.xScale(d.x) + (self.xScale.rangeBand() / 2); }, y = function (d) { return self.yScale(d.y); }, line = d3.svg.line() .x(x) .interpolate(inter), area = d3.svg.area() .x(x) .y1(self.yZero) .interpolate(inter), container, fills, paths; function datum(d) { return [d.data]; } container = self._g.selectAll(selector + className) .data(data, function (d) { return d.className; }); container.enter().insert('g', insertBefore) .attr('data-index', zIndex) .attr('class', function (d, i) { var cl = _.uniq((className + d.className).split('.')).join(' '); return cl + ' line ' + _visutils.colorClass(this, i); }); fills = container.selectAll('path.fill') .data(datum); fills.enter().append('path') .attr('class', 'fill') .style('opacity', 0) .attr('d', area.y0(y)); paths = container.selectAll('path.line') .data(datum); paths.enter().append('path') .attr('class', 'line') .style('opacity', 0) .attr('d', line.y(y)); storage.lineContainers = container; storage.lineFills = fills; storage.linePaths = paths; storage.lineX = x; storage.lineY = y; storage.lineA = area; storage.line = line; } function update(self, storage, timing) { storage.lineContainers .attr('class', function (d, i) { return _visutils.colorClass(this, i); }); storage.lineFills.transition().duration(timing) .style('opacity', 1) .attr('d', storage.lineA.y0(storage.lineY)); storage.linePaths.transition().duration(timing) .style('opacity', 1) .attr('d', storage.line.y(storage.lineY)); } function exit(self, storage) { storage.linePaths.exit() .style('opacity', 0) .remove(); storage.lineFills.exit() .style('opacity', 0) .remove(); storage.lineContainers.exit() .remove(); } function destroy(self, storage, timing) { storage.linePaths.transition().duration(timing) .style('opacity', 0); storage.lineFills.transition().duration(timing) .style('opacity', 0); } _vis.line = { enter: enter, update: update, exit: exit, destroy: destroy }; }()); (function () { var line = _vis.line; function enter(self, storage, className, data, callbacks) { var circles; line.enter(self, storage, className, data, callbacks); circles = storage.lineContainers.selectAll('circle') .data(function (d) { return d.data; }, function (d) { return d.x; }); circles.enter().append('circle') .style('opacity', 0) .attr('cx', storage.lineX) .attr('cy', storage.lineY) .attr('r', 5) .on('mouseover', callbacks.mouseover) .on('mouseout', callbacks.mouseout) .on('click', callbacks.click); storage.lineCircles = circles; } function update(self, storage, timing) { line.update.apply(null, _.toArray(arguments)); storage.lineCircles.transition().duration(timing) .style('opacity', 1) .attr('cx', storage.lineX) .attr('cy', storage.lineY); } function exit(self, storage) { storage.lineCircles.exit() .remove(); line.exit.apply(null, _.toArray(arguments)); } function destroy(self, storage, timing) { line.destroy.apply(null, _.toArray(arguments)); if (!storage.lineCircles) { return; } storage.lineCircles.transition().duration(timing) .style('opacity', 0); } _vis['line-dotted'] = { enter: enter, update: update, exit: exit, destroy: destroy }; }()); (function () { var line = _vis['line-dotted']; function enter(self, storage, className, data, callbacks) { line.enter(self, storage, className, data, callbacks); } function _accumulate_data(data) { function reduce(memo, num) { return memo + num.y; } var nData = _.map(data, function (set) { var i = set.data.length, d = _.clone(set.data); set = _.clone(set); while (i) { i -= 1; // Need to clone here, otherwise we are actually setting the same // data onto the original data set. d[i] = _.clone(set.data[i]); d[i].y0 = set.data[i].y; d[i].y = _.reduce(_.first(set.data, i), reduce, set.data[i].y); } return _.extend(set, { data: d }); }); return nData; } function _resetData(self) { if (!self.hasOwnProperty('cumulativeOMainData')) { return; } self._mainData = self.cumulativeOMainData; delete self.cumulativeOMainData; self._compData = self.cumulativeOCompData; delete self.cumulativeOCompData; } function preUpdateScale(self, data) { _resetData(self); self.cumulativeOMainData = self._mainData; self._mainData = _accumulate_data(self._mainData); self.cumulativeOCompData = self._compData; self._compData = _accumulate_data(self._compData); } function destroy(self, storage, timing) { _resetData(self); line.destroy.apply(null, _.toArray(arguments)); } _vis.cumulative = { preUpdateScale: preUpdateScale, enter: enter, update: line.update, exit: line.exit, destroy: destroy }; }()); var emptyData = [[]], defaults = { // User interaction callbacks mouseover: function (data, i) {}, mouseout: function (data, i) {}, click: function (data, i) {}, // Padding between the axes and the contents of the chart axisPaddingTop: 0, axisPaddingRight: 0, axisPaddingBottom: 5, axisPaddingLeft: 20, // Padding around the edge of the chart (space for axis labels, etc) paddingTop: 0, paddingRight: 0, paddingBottom: 20, paddingLeft: 60, // Axis tick formatting tickHintX: 10, tickFormatX: function (x) { return x; }, tickHintY: 10, tickFormatY: function (y) { return y; }, // Min/Max Axis Values xMin: null, xMax: null, yMin: null, yMax: null, // Pre-format input data dataFormatX: function (x) { return x; }, dataFormatY: function (y) { return y; }, unsupported: function (selector) { d3.select(selector).text('SVG is not supported on your browser'); }, // Callback functions if no data empty: function (self, selector, d) {}, notempty: function (self, selector) {}, timing: 750, // Line interpolation interpolation: 'monotone', // Data sorting sortX: function (a, b) { return (!a.x && !b.x) ? 0 : (a.x < b.x) ? -1 : 1; } }; // What/how should the warning/error be presented? function svgEnabled() { var d = document; return (!!d.createElementNS && !!d.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect); } /** * Creates a new chart * * @param string type The drawing type for the main data * @param array data Data to render in the chart * @param string selector CSS Selector for the parent element for the chart * @param object options Optional. See `defaults` for options * * Examples: * var data = { * "main": [ * { * "data": [ * { * "x": "2012-08-09T07:00:00.522Z", * "y": 68 * }, * { * "x": "2012-08-10T07:00:00.522Z", * "y": 295 * }, * { * "x": "2012-08-11T07:00:00.522Z", * "y": 339 * }, * ], * "className": ".foo" * } * ], * "xScale": "ordinal", * "yScale": "linear", * "comp": [ * { * "data": [ * { * "x": "2012-08-09T07:00:00.522Z", * "y": 288 * }, * { * "x": "2012-08-10T07:00:00.522Z", * "y": 407 * }, * { * "x": "2012-08-11T07:00:00.522Z", * "y": 459 * } * ], * "className": ".comp.comp_foo", * "type": "line-arrowed" * } * ] * }, * myChart = new Chart('bar', data, '#chart'); * */ function xChart(type, data, selector, options) { var self = this, resizeLock; self._options = options = _.defaults(options || {}, defaults); if (svgEnabled() === false) { return options.unsupported(selector); } self._selector = selector; self._container = d3.select(selector); self._drawSvg(); self._mainStorage = {}; self._compStorage = {}; data = _.clone(data); if (type && !data.type) { data.type = type; } self.setData(data); d3.select(window).on('resize.for.' + selector, function () { if (resizeLock) { clearTimeout(resizeLock); } resizeLock = setTimeout(function () { resizeLock = null; self._resize(); }, 500); }); } /** * Add a visualization type * * @param string type Unique key/name used with setType * @param object vis object map of vis methods */ xChart.setVis = function (type, vis) { if (_vis.hasOwnProperty(type)) { throw 'Cannot override vis type "' + type + '".'; } _vis[type] = vis; }; /** * Get a clone of a visualization * Useful for extending vis functionality * * @param string type Unique key/name of the vis */ xChart.getVis = function (type) { if (!_vis.hasOwnProperty(type)) { throw 'Vis type "' + type + '" does not exist.'; } return _.clone(_vis[type]); }; xChart.setScale = function (name, fn) { if (_scales.hasOwnProperty(name)) { throw 'Scale type "' + name + '" already exists.'; } _scales[name] = fn; }; xChart.getScale = function (name) { if (!_scales.hasOwnProperty(name)) { throw 'Scale type "' + name + '" does not exist.'; } return _scales[name]; }; xChart.visutils = _visutils; _.defaults(xChart.prototype, { /** * Set or change the drawing type for the main data. * * @param string type Must be an available drawing type * */ setType: function (type, skipDraw) { var self = this; if (self._type && type === self._type) { return; } if (!_vis.hasOwnProperty(type)) { throw 'Vis type "' + type + '" is not defined.'; } if (self._type) { self._destroy(self._vis, self._mainStorage); } self._type = type; self._vis = _vis[type]; if (!skipDraw) { self._draw(); } }, /** * Set and update the data for the chart. Optionally skip drawing. * * @param object data New data. See new xChart example for format * */ setData: function (data) { var self = this, o = self._options, nData = _.clone(data); if (!data.hasOwnProperty('main')) { throw 'No "main" key found in given chart data.'; } switch (data.type) { case 'bar': // force the xScale to be ordinal data.xScale = 'ordinal'; break; case undefined: data.type = self._type; break; } o.xMin = (isNaN(parseInt(data.xMin, 10))) ? o.xMin : data.xMin; o.xMax = (isNaN(parseInt(data.xMax, 10))) ? o.xMax : data.xMax; o.yMin = (isNaN(parseInt(data.yMin, 10))) ? o.yMin : data.yMin; o.yMax = (isNaN(parseInt(data.yMax, 10))) ? o.yMax : data.yMax; if (self._vis) { self._destroy(self._vis, self._mainStorage); } self.setType(data.type, true); function _mapData(set) { var d = _.map(_.clone(set.data), function (p) { var np = _.clone(p); if (p.hasOwnProperty('x')) { np.x = o.dataFormatX(p.x); } if (p.hasOwnProperty('y')) { np.y = o.dataFormatY(p.y); } return np; }).sort(o.sortX); return _.extend(_.clone(set), { data: d }); } nData.main = _.map(nData.main, _mapData); self._mainData = nData.main; self._xScaleType = nData.xScale; self._yScaleType = nData.yScale; if (nData.hasOwnProperty('comp')) { nData.comp = _.map(nData.comp, _mapData); self._compData = nData.comp; } else { self._compData = []; } self._draw(); }, /** * Change the scale of an axis * * @param string axis Name of an axis. One of 'x' or 'y' * @param string type Name of the scale type * */ setScale: function (axis, type) { var self = this; switch (axis) { case 'x': self._xScaleType = type; break; case 'y': self._yScaleType = type; break; default: throw 'Cannot change scale of unknown axis "' + axis + '".'; } self._draw(); }, /** * Create the SVG element and g container. Resize if necessary. */ _drawSvg: function () { var self = this, c = self._container, options = self._options, width = parseInt(c.style('width').replace('px', ''), 10), height = parseInt(c.style('height').replace('px', ''), 10), svg, g, gScale; svg = c.selectAll('svg') .data(emptyData); svg.enter().append('svg') // Inherit the height and width from the parent element .attr('height', height) .attr('width', width) .attr('class', 'xchart'); svg.transition() .attr('width', width) .attr('height', height); g = svg.selectAll('g') .data(emptyData); g.enter().append('g') .attr( 'transform', 'translate(' + options.paddingLeft + ',' + options.paddingTop + ')' ); gScale = g.selectAll('g.scale') .data(emptyData); gScale.enter().append('g') .attr('class', 'scale'); self._svg = svg; self._g = g; self._gScale = gScale; self._height = height - options.paddingTop - options.paddingBottom - options.axisPaddingTop - options.axisPaddingBottom; self._width = width - options.paddingLeft - options.paddingRight - options.axisPaddingLeft - options.axisPaddingRight; }, /** * Resize the visualization */ _resize: function (event) { var self = this; self._drawSvg(); self._draw(); }, /** * Draw the x and y axes */ _drawAxes: function () { if (this._noData) { return; } var self = this, o = self._options, t = self._gScale.transition().duration(o.timing), xTicks = o.tickHintX, yTicks = o.tickHintY, bottom = self._height + o.axisPaddingTop + o.axisPaddingBottom, zeroLine = d3.svg.line().x(function (d) { return d; }), zLine, zLinePath, xAxis, xRules, yAxis, yRules, labels; xRules = d3.svg.axis() .scale(self.xScale) .ticks(xTicks) .tickSize(-self._height) .tickFormat(o.tickFormatX) .orient('bottom'); xAxis = self._gScale.selectAll('g.axisX') .data(emptyData); xAxis.enter().append('g') .attr('class', 'axis axisX') .attr('transform', 'translate(0,' + bottom + ')'); xAxis.call(xRules); labels = self._gScale.selectAll('.axisX g')[0]; if (labels.length > (self._width / 80)) { labels.sort(function (a, b) { var r = /translate\(([^,)]+)/; a = a.getAttribute('transform').match(r); b = b.getAttribute('transform').match(r); return parseFloat(a[1], 10) - parseFloat(b[1], 10); }); d3.selectAll(labels) .filter(function (d, i) { return i % (Math.ceil(labels.length / xTicks) + 1); }) .remove(); } yRules = d3.svg.axis() .scale(self.yScale) .ticks(yTicks) .tickSize(-self._width - o.axisPaddingRight - o.axisPaddingLeft) .tickFormat(o.tickFormatY) .orient('left'); yAxis = self._gScale.selectAll('g.axisY') .data(emptyData); yAxis.enter().append('g') .attr('class', 'axis axisY') .attr('transform', 'translate(0,0)'); t.selectAll('g.axisY') .call(yRules); // zero line zLine = self._gScale.selectAll('g.axisZero') .data([[]]); zLine.enter().append('g') .attr('class', 'axisZero'); zLinePath = zLine.selectAll('line') .data([[]]); zLinePath.enter().append('line') .attr('x1', 0) .attr('x2', self._width + o.axisPaddingLeft + o.axisPaddingRight) .attr('y1', self.yZero) .attr('y2', self.yZero); zLinePath.transition().duration(o.timing) .attr('y1', self.yZero) .attr('y2', self.yZero); }, /** * Update the x and y scales (used when drawing) * * Optional methods in drawing types: * preUpdateScale * postUpdateScale * * Example implementation in vis type: * * function postUpdateScale(self, scaleData, mainData, compData) { * self.xScale2 = d3.scale.ordinal() * .domain(d3.range(0, mainData.length)) * .rangeRoundBands([0, self.xScale.rangeBand()], 0.08); * } * */ _updateScale: function () { var self = this, _unionData = function () { return _.union(self._mainData, self._compData); }, scaleData = _unionData(), vis = self._vis, scale, min; delete self.xScale; delete self.yScale; delete self.yZero; if (vis.hasOwnProperty('preUpdateScale')) { vis.preUpdateScale(self, scaleData, self._mainData, self._compData); } // Just in case preUpdateScale modified scaleData = _unionData(); scale = _scales.xy(self, scaleData, self._xScaleType, self._yScaleType); self.xScale = scale.x; self.yScale = scale.y; min = self.yScale.domain()[0]; self.yZero = (min > 0) ? self.yScale(min) : self.yScale(0); if (vis.hasOwnProperty('postUpdateScale')) { vis.postUpdateScale(self, scaleData, self._mainData, self._compData); } }, /** * Create (Enter) the elements for the vis * * Required method * * Example implementation in vis type: * * function enter(self, data, callbacks) { * var foo = self._g.selectAll('g.foobar') * .data(data); * foo.enter().append('g') * .attr('class', 'foobar'); * self.foo = foo; * } */ _enter: function (vis, storage, data, className) { var self = this, callbacks = { click: self._options.click, mouseover: self._options.mouseover, mouseout: self._options.mouseout }; self._checkVisMethod(vis, 'enter'); vis.enter(self, storage, className, data, callbacks); }, /** * Update the elements opened by the select method * * Required method * * Example implementation in vis type: * * function update(self, timing) { * self.bars.transition().duration(timing) * .attr('width', self.xScale2.rangeBand()) * .attr('height', function (d) { * return self.yScale(d.y); * }); * } */ _update: function (vis, storage) { var self = this; self._checkVisMethod(vis, 'update'); vis.update(self, storage, self._options.timing); }, /** * Remove or transition out the elements that no longer have data * * Required method * * Example implementation in vis type: * * function exit(self) { * self.bars.exit().remove(); * } */ _exit: function (vis, storage) { var self = this; self._checkVisMethod(vis, 'exit'); vis.exit(self, storage, self._options.timing); }, /** * Destroy the current vis type (transition to new type) * * Required method * * Example implementation in vis type: * * function destroy(self, timing) { * self.bars.transition().duration(timing) * attr('height', 0); * delete self.bars; * } */ _destroy: function (vis, storage) { var self = this; self._checkVisMethod(vis, 'destroy'); try { vis.destroy(self, storage, self._options.timing); } catch (e) {} }, /** * Draw the visualization */ _draw: function () { var self = this, o = self._options, comp, compKeys; self._noData = _.flatten(_.pluck(self._mainData, 'data') .concat(_.pluck(self._compData, 'data'))).length === 0; self._updateScale(); self._drawAxes(); self._enter(self._vis, self._mainStorage, self._mainData, '.main'); self._exit(self._vis, self._mainStorage); self._update(self._vis, self._mainStorage); comp = _.chain(self._compData).groupBy(function (d) { return d.type; }); compKeys = comp.keys(); // Find old comp vis items and remove any that no longer exist _.each(self._compStorage, function (d, key) { if (-1 === compKeys.indexOf(key).value()) { var vis = _vis[key]; self._enter(vis, d, [], '.comp.' + key.replace(/\W+/g, '')); self._exit(vis, d); } }); comp.each(function (d, key) { var vis = _vis[key], storage; if (!self._compStorage.hasOwnProperty(key)) { self._compStorage[key] = {}; } storage = self._compStorage[key]; self._enter(vis, storage, d, '.comp.' + key.replace(/\W+/g, '')); self._exit(vis, storage); self._update(vis, storage); }); if (self._noData) { o.empty(self, self._selector, self._mainData); } else { o.notempty(self, self._selector); } }, /** * Ensure drawing method exists */ _checkVisMethod: function (vis, method) { var self = this; if (!vis[method]) { throw 'Required method "' + method + '" not found on vis type "' + self._type + '".'; } } }); if (typeof define === 'function' && define.amd && typeof define.amd === 'object') { define(function () { return xChart; }); return; } window.xChart = xChart; }()); xcharts/xcharts-demo-1.js 0000644 00000003161 15030376016 0011314 0 ustar 00 $(function() { var tt = document.createElement('div'), leftOffset = -(~~$('html').css('padding-left').replace('px', '') + ~~$('body').css('margin-left').replace('px', '')), topOffset = -32; tt.className = 'ex-tooltip'; document.body.appendChild(tt); var data = { "xScale": "time", "yScale": "linear", "main": [{ "className": ".pizza", "data": [{ "x": "2012-11-05", "y": 6 }, { "x": "2012-11-06", "y": 6 }, { "x": "2012-11-07", "y": 8 }, { "x": "2012-11-08", "y": 3 }, { "x": "2012-11-09", "y": 4 }, { "x": "2012-11-10", "y": 9 }, { "x": "2012-11-11", "y": 6 }] }] }; var opts = { "dataFormatX": function(x) { return d3.time.format('%Y-%m-%d').parse(x); }, "tickFormatX": function(x) { return d3.time.format('%A')(x); }, "mouseover": function(d, i) { var pos = $(this).offset(); $(tt).text(d3.time.format('%A')(d.x) + ': ' + d.y) .css({ top: topOffset + pos.top, left: pos.left + leftOffset }) .show(); }, "mouseout": function(x) { $(tt).hide(); } }; var myChart = new xChart('line-dotted', data, '#example4', opts); }); xcharts/xcharts.css 0000644 00000010671 15030376016 0010414 0 ustar 00 /* xCharts */ .xchart .line { stroke-width: 3px; fill: none; } .xchart .fill { stroke-width: 0; } .xchart circle { stroke: #FFF; stroke-width: 3px; } .xchart .axis .domain { fill: none; } .xchart .axis .tick line { stroke: #EEE; stroke-width: 1px; } .xchart .axis text { fill: #666; font-size: 12px; } .xchart .color2 .line { stroke: #f26522; } .xchart .color2 .line .fill { pointer-events: none; } .xchart .color2 rect, .xchart .color2 circle { fill: #f26522; } .xchart .color2 .fill { fill: rgba(242, 101, 34, 0.1); } .xchart .color2.comp .line { stroke: #f9b99a; } .xchart .color2.comp rect { fill: #f9b99a; } .xchart .color2.comp .fill { display: none; } .xchart .color2.comp circle, .xchart .color2.comp .pointer { fill: #f9b99a; } .xchart .color3 .line { stroke: #c6080d; } .xchart .color3 .line .fill { pointer-events: none; } .xchart .color3 rect, .xchart .color3 circle { fill: #c6080d; } .xchart .color3 .fill { fill: rgba(198, 8, 13, 0.1); } .xchart .color3.comp .line { stroke: #f8555a; } .xchart .color3.comp rect { fill: #f8555a; } .xchart .color3.comp .fill { display: none; } .xchart .color3.comp circle, .xchart .color3.comp .pointer { fill: #f8555a; } .xchart .color4 .line { stroke: #672d8b; } .xchart .color4 .line .fill { pointer-events: none; } .xchart .color4 rect, .xchart .color4 circle { fill: #672d8b; } .xchart .color4 .fill { fill: rgba(103, 45, 139, 0.1); } .xchart .color4.comp .line { stroke: #a869ce; } .xchart .color4.comp rect { fill: #a869ce; } .xchart .color4.comp .fill { display: none; } .xchart .color4.comp circle, .xchart .color4.comp .pointer { fill: #a869ce; } .xchart .color5 .line { stroke: #ce1797; } .xchart .color5 .line .fill { pointer-events: none; } .xchart .color5 rect, .xchart .color5 circle { fill: #ce1797; } .xchart .color5 .fill { fill: rgba(206, 23, 151, 0.1); } .xchart .color5.comp .line { stroke: #f075cb; } .xchart .color5.comp rect { fill: #f075cb; } .xchart .color5.comp .fill { display: none; } .xchart .color5.comp circle, .xchart .color5.comp .pointer { fill: #f075cb; } .xchart .color6 .line { stroke: #d9ce00; } .xchart .color6 .line .fill { pointer-events: none; } .xchart .color6 rect, .xchart .color6 circle { fill: #d9ce00; } .xchart .color6 .fill { fill: rgba(217, 206, 0, 0.1); } .xchart .color6.comp .line { stroke: #fff75a; } .xchart .color6.comp rect { fill: #fff75a; } .xchart .color6.comp .fill { display: none; } .xchart .color6.comp circle, .xchart .color6.comp .pointer { fill: #fff75a; } .xchart .color7 .line { stroke: #754c24; } .xchart .color7 .line .fill { pointer-events: none; } .xchart .color7 rect, .xchart .color7 circle { fill: #754c24; } .xchart .color7 .fill { fill: rgba(117, 76, 36, 0.1); } .xchart .color7.comp .line { stroke: #c98c50; } .xchart .color7.comp rect { fill: #c98c50; } .xchart .color7.comp .fill { display: none; } .xchart .color7.comp circle, .xchart .color7.comp .pointer { fill: #c98c50; } .xchart .color8 .line { stroke: #2eb9b4; } .xchart .color8 .line .fill { pointer-events: none; } .xchart .color8 rect, .xchart .color8 circle { fill: #2eb9b4; } .xchart .color8 .fill { fill: rgba(46, 185, 180, 0.1); } .xchart .color8.comp .line { stroke: #86e1de; } .xchart .color8.comp rect { fill: #86e1de; } .xchart .color8.comp .fill { display: none; } .xchart .color8.comp circle, .xchart .color8.comp .pointer { fill: #86e1de; } .xchart .color9 .line { stroke: #0e2e42; } .xchart .color9 .line .fill { pointer-events: none; } .xchart .color9 rect, .xchart .color9 circle { fill: #0e2e42; } .xchart .color9 .fill { fill: rgba(14, 46, 66, 0.1); } .xchart .color9.comp .line { stroke: #2477ab; } .xchart .color9.comp rect { fill: #2477ab; } .xchart .color9.comp .fill { display: none; } .xchart .color9.comp circle, .xchart .color9.comp .pointer { fill: #2477ab; } xcharts/xcharts-demo-2.js 0000644 00000014141 15030376016 0011315 0 ustar 00 $(function() { var tt = document.createElement('div'), leftOffset = -(~~$('html').css('padding-left').replace('px', '') + ~~$('body').css('margin-left').replace('px', '')), topOffset = 0; tt.className = 'tooltip top fade in'; document.body.appendChild(tt); var data = [{ "xScale": "ordinal", "comp": [], "main": [{ "className": ".main.l1", "data": [{ "y": 15, "x": "2012-11-19T00:00:00" }, { "y": 11, "x": "2012-11-20T00:00:00" }, { "y": 8, "x": "2012-11-21T00:00:00" }, { "y": 10, "x": "2012-11-22T00:00:00" }, { "y": 1, "x": "2012-11-23T00:00:00" }, { "y": 6, "x": "2012-11-24T00:00:00" }, { "y": 8, "x": "2012-11-25T00:00:00" }] }, { "className": ".main.l2", "data": [{ "y": 29, "x": "2012-11-19T00:00:00" }, { "y": 33, "x": "2012-11-20T00:00:00" }, { "y": 13, "x": "2012-11-21T00:00:00" }, { "y": 16, "x": "2012-11-22T00:00:00" }, { "y": 7, "x": "2012-11-23T00:00:00" }, { "y": 18, "x": "2012-11-24T00:00:00" }, { "y": 8, "x": "2012-11-25T00:00:00" }] }], "type": "line-dotted", "yScale": "linear" }, { "xScale": "ordinal", "comp": [], "main": [{ "className": ".main.l1", "data": [{ "y": 12, "x": "2012-11-19T00:00:00" }, { "y": 18, "x": "2012-11-20T00:00:00" }, { "y": 8, "x": "2012-11-21T00:00:00" }, { "y": 7, "x": "2012-11-22T00:00:00" }, { "y": 6, "x": "2012-11-23T00:00:00" }, { "y": 12, "x": "2012-11-24T00:00:00" }, { "y": 8, "x": "2012-11-25T00:00:00" }] }, { "className": ".main.l2", "data": [{ "y": 29, "x": "2012-11-19T00:00:00" }, { "y": 33, "x": "2012-11-20T00:00:00" }, { "y": 13, "x": "2012-11-21T00:00:00" }, { "y": 16, "x": "2012-11-22T00:00:00" }, { "y": 7, "x": "2012-11-23T00:00:00" }, { "y": 18, "x": "2012-11-24T00:00:00" }, { "y": 8, "x": "2012-11-25T00:00:00" }] }], "type": "cumulative", "yScale": "linear" }, { "xScale": "ordinal", "comp": [], "main": [{ "className": ".main.l1", "data": [{ "y": 12, "x": "2012-11-19T00:00:00" }, { "y": 18, "x": "2012-11-20T00:00:00" }, { "y": 8, "x": "2012-11-21T00:00:00" }, { "y": 7, "x": "2012-11-22T00:00:00" }, { "y": 6, "x": "2012-11-23T00:00:00" }, { "y": 12, "x": "2012-11-24T00:00:00" }, { "y": 8, "x": "2012-11-25T00:00:00" }] }, { "className": ".main.l2", "data": [{ "y": 29, "x": "2012-11-19T00:00:00" }, { "y": 33, "x": "2012-11-20T00:00:00" }, { "y": 13, "x": "2012-11-21T00:00:00" }, { "y": 16, "x": "2012-11-22T00:00:00" }, { "y": 7, "x": "2012-11-23T00:00:00" }, { "y": 18, "x": "2012-11-24T00:00:00" }, { "y": 8, "x": "2012-11-25T00:00:00" }] }], "type": "bar", "yScale": "linear" }]; var order = [0, 1, 0, 2], i = 0, xFormat = d3.time.format('%A'), chart = new xChart('line-dotted', data[order[i]], '#example-vis', { axisPaddingTop: 5, dataFormatX: function(x) { return new Date(x); }, tickFormatX: function(x) { return xFormat(x); }, mouseover: function(d, i) { var pos = $(this).offset(); $(tt).html('<div class="arrow"></div><div class="tooltip-inner">' + d3.time.format('%A')(d.x) + ': ' + d.y + '</div>') .css({ top: topOffset + pos.top, left: pos.left + leftOffset }) .show(); }, mouseout: function(x) { $(tt).hide(); }, timing: 1250 }), rotateTimer, toggles = d3.selectAll('#upd-chart a'), t = 3500; function updateChart(i) { var d = data[i]; chart.setData(d); toggles.classed('active', function() { return (d3.select(this).attr('data-type') === d.type); }); return d; } toggles.on('click', function(d, i) { clearTimeout(rotateTimer); updateChart(i); }); function rotateChart() { i += 1; i = (i >= order.length) ? 0 : i; var d = updateChart(order[i]); rotateTimer = setTimeout(rotateChart, t); } rotateTimer = setTimeout(rotateChart, t); }()); xcharts/xcharts-demo.js 0000644 00000003766 15030376016 0011171 0 ustar 00 $(function() { var data = { "xScale": "ordinal", "yScale": "linear", "main": [{ "className": ".pizza", "data": [{ "x": "Pepperoni", "y": 4 }, { "x": "Cheese", "y": 8 }] }] }; var myChart = new xChart('bar', data, '#example1'); }); $(function() { var tt = document.createElement('div'), leftOffset = -(~~$('html').css('padding-left').replace('px', '') + ~~$('body').css('margin-left').replace('px', '')), topOffset = -32; tt.className = 'ex-tooltip'; document.body.appendChild(tt); var data = { "xScale": "time", "yScale": "linear", "main": [{ "className": ".pizza", "data": [{ "x": "2012-11-05", "y": 6 }, { "x": "2012-11-06", "y": 6 }, { "x": "2012-11-07", "y": 8 }, { "x": "2012-11-08", "y": 3 }, { "x": "2012-11-09", "y": 4 }, { "x": "2012-11-10", "y": 9 }, { "x": "2012-11-11", "y": 6 }] }] }; var opts = { "dataFormatX": function(x) { return d3.time.format('%Y-%m-%d').parse(x); }, "tickFormatX": function(x) { return d3.time.format('%A')(x); }, "mouseover": function(d, i) { var pos = $(this).offset(); $(tt).text(d3.time.format('%A')(d.x) + ': ' + d.y) .css({ top: topOffset + pos.top, left: pos.left + leftOffset }) .show(); }, "mouseout": function(x) { $(tt).hide(); } }; var myChart = new xChart('line-dotted', data, '#example4', opts); });
| ver. 1.4 |
Github
|
.
| PHP 8.2.28 | Generation time: 0.04 |
proxy
|
phpinfo
|
Settings