├── docs ├── layout │ ├── end.html │ └── begin.html ├── intro.md ├── prettify.css ├── index.md ├── examples.html ├── index.html ├── ico.min.js └── ico.js ├── .gitignore ├── src ├── end.js ├── start.js ├── graphs │ ├── sparkbar.js │ ├── line.js │ ├── sparkline.js │ ├── horizontal_bar.js │ ├── bar.js │ └── base.js ├── base.js ├── normaliser.js └── helpers.js ├── test ├── base.test.js └── browser │ ├── base.test.js │ ├── line_graph.test.js │ ├── index.html │ ├── sparkbar.test.js │ ├── normalisation.test.js │ └── qunit │ └── qunit.css ├── package.json ├── Makefile ├── MIT-LICENSE ├── README.textile ├── History.md ├── ico.min.js └── ico.js /docs/layout/end.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swo 2 | *.swp 3 | .DS_Store 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /src/end.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Assign the Ico object as a global property. 3 | */ 4 | global.Ico = Ico; 5 | 6 | if (typeof exports !== 'undefined') { 7 | module.exports = Ico; 8 | } 9 | }(typeof window === 'undefined' ? this : window)); 10 | 11 | -------------------------------------------------------------------------------- /test/base.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , Ico = require(__dirname + '/../ico.js'); 3 | 4 | module.exports = { 5 | 'test a simple graph': function() { 6 | var element = { style: { width: 100, height: 100, backgroundColor: 'blue' } }, 7 | graph = new Ico.BaseGraph(element, [10, 20, 15, 25, 40, 35, 50, 5, 15], { draw: false }); 8 | 9 | assert.deepEqual(graph.options.labels, [1, 2, 3, 4, 5, 6, 7, 8, 9]); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/browser/base.test.js: -------------------------------------------------------------------------------- 1 | module('Base'); 2 | 3 | test('a one-dimensional array', function() { 4 | var values = [10, 20, 15]; 5 | deepEqual(Ico.Base.flatten(values), values); 6 | }); 7 | 8 | test('an array containing arrays', function() { 9 | var values = [10, [20, 3], [1, 2, 3]]; 10 | deepEqual(Ico.Base.flatten(values), [10, 20, 3, 1, 2, 3]); 11 | }); 12 | 13 | test('a deeper set of arrays', function() { 14 | var values = [10, [20, [1, 2, [4, 5, 3]]], [1, 2, 3]]; 15 | deepEqual(Ico.Base.flatten(values), [10, 20, 1, 2, 4, 5, 3, 1, 2, 3]); 16 | }); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name": "ico" 2 | , "description": "A graph plotting library" 3 | , "version": "0.3.10" 4 | , "url": "http://alexyoung.github.com/ico/" 5 | , "author": "Alex R. Young " 6 | , "engines": ["node >= 0.4.0"] 7 | , "main": "./ico.js" 8 | , "devDependencies": { 9 | "dox": "latest" 10 | , "jake": "latest" 11 | , "jsmin": "latest" 12 | , "jslint": "latest" 13 | , "uglify-js": "latest" 14 | , "expresso": "latest" 15 | } 16 | , "repository": { 17 | "type" : "git" 18 | , "url" : "https://github.com/alexyoung/ico.git" 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/start.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Ico 3 | * Copyright (C) 2009-2011 Alex R. Young 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * The Ico object. 9 | */ 10 | (function(global) { 11 | var Ico = { 12 | VERSION: '0.3.10', 13 | 14 | /** 15 | * Rounds a float to the specified number of decimal places. 16 | * 17 | * @param {Float} num A number to round 18 | * @param {Integer} dec The number of decimal places 19 | * @returns {Float} The rounded result 20 | */ 21 | round: function(num, dec) { 22 | var result = Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec); 23 | return result; 24 | } 25 | }; 26 | 27 | -------------------------------------------------------------------------------- /test/browser/line_graph.test.js: -------------------------------------------------------------------------------- 1 | module('LineGraph'); 2 | 3 | test('LineGraph creation', function() { 4 | var element = document.getElementById('linegraph'), 5 | linegraph = new Ico.LineGraph(element, { 6 | one: [30, 5, 1, 10, 15, 18, 20, 25, 1], 7 | two: [10, 9, 3, 30, 1, 10, 5, 33, 33], 8 | three: [5, 4, 10, 1, 30, 11, 33, 12, 22]}, 9 | { markers: 'circle', 10 | colours: { one: '#990000', two: '#009900', three: '#000099'}, 11 | labels: ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'], 12 | meanline: true, 13 | grid: true, 14 | width: 600, 15 | height: 345, 16 | background_colour: '#fff' 17 | }); 18 | 19 | ok(linegraph); 20 | }); 21 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | Ico is a JavaScript graph library that uses [Raphael](http://raphaeljs.com/) to draw. This means it can draw charts in multiple browsers (including IE). 2 | 3 | Get it at GitHub: [alexyoung / ico](https://github.com/alexyoung/ico) or [view examples](http://alexyoung.github.com/ico/examples.html). 4 | 5 | The API uses a data parameter then an additional option for customisation: 6 | 7 | new Ico.BarGraph(dom_element, data, options); 8 | 9 | An array or object can be passed as data: 10 | 11 | new Ico.BarGraph(dom_element, [1, 2, 3, 4], { grid: true }); 12 | 13 | new Ico.BarGraph($('dom_element'), { 14 | shoe_size: [1, 1, 1, 0, 2, 4, 6, 8, 3, 9, 6] 15 | }, 16 | { colours: {shoe_size: '#990000' }, 17 | grid: true }); 18 | 19 | ## Support 20 | 21 | [Donate](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=support%40helicoid%2enet&lc=GB&item_name=Helicoid%20Limited&no_note=0&cn=Add%20special%20instructions%20to%20the%20seller&no_shipping=2¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted) 22 | -------------------------------------------------------------------------------- /src/graphs/sparkbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Draws spark bar graphs. 3 | * 4 | * Example: 5 | * 6 | * new Ico.SparkBar($('sparkline_2'), 7 | * [1, 5, 10, 15, 20, 15, 10, 15, 30, 15, 10], 8 | * { width: 30, height: 14, background_colour: '#ccc' }); 9 | * 10 | */ 11 | Ico.SparkBar = function() { this.initialize.apply(this, arguments); }; 12 | Helpers.extend(Ico.SparkBar.prototype, Ico.SparkLine.prototype); 13 | Helpers.extend(Ico.SparkBar.prototype, { 14 | calculateStep: function() { 15 | return this.options.width / validStepDivider(this.data.length); 16 | }, 17 | 18 | drawLines: function(label, colour, data) { 19 | var width = this.step > 2 ? this.step - 1 : this.step, 20 | x = width, 21 | pathString = '', 22 | i = 0; 23 | for (i = 0; i < data.length; i++) { 24 | pathString += 'M' + x + ',' + (this.options.height - data[i]); 25 | pathString += 'L' + x + ',' + this.options.height; 26 | x = x + this.step; 27 | } 28 | this.paper.path(pathString).attr({ stroke: colour, 'stroke-width': width }); 29 | } 30 | }); 31 | 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC = src/start.js src/helpers.js src/normaliser.js src/base.js src/graphs/base.js src/graphs/bar.js src/graphs/horizontal_bar.js src/graphs/line.js src/graphs/sparkline.js src/graphs/sparkbar.js src/end.js 2 | 3 | lint: build 4 | ./node_modules/.bin/jslint --onevar false ico.js 5 | 6 | build: $(SRC) 7 | @cat $^ > ico.js 8 | @cp ico.js docs/ico.js 9 | 10 | min: build 11 | @./node_modules/.bin/uglifyjs --no-mangle ico.js > ico.min.js 12 | @cp ico.min.js docs/ico.min.js 13 | 14 | test: build 15 | ./node_modules/.bin/expresso 16 | 17 | docs: min 18 | @markdown docs/index.md \ 19 | | cat docs/layout/begin.html - docs/layout/end.html \ 20 | > docs/index.html 21 | @cp ico.min.js docs/ico.min.js 22 | @cp ico.js docs/ico.js 23 | @cp raphael.js docs/raphael.js 24 | 25 | publishdocs: 26 | $(eval PARENT_SHA := $(shell git show-ref -s refs/heads/gh-pages)) 27 | $(eval DOC_SHA := $(shell git ls-tree -d HEAD docs | awk '{print $$3}')) 28 | $(eval COMMIT := $(shell echo "Auto-update docs." | git commit-tree $(DOC_SHA) -p $(PARENT_SHA))) 29 | @git update-ref refs/heads/gh-pages $(COMMIT) 30 | 31 | .PHONY: lint docs test publishdocs 32 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Alex R. Young 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /test/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ico Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

Ico Tests

18 |

19 |
20 |

21 |
    22 |
    23 |
    24 |
    25 |
    26 |
    27 |
    28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/prettify.css: -------------------------------------------------------------------------------- 1 | /* Pretty printing styles. Used with prettify.js. */ 2 | 3 | .str { color: #080; } 4 | .kwd { color: #008; } 5 | .com { color: #800; } 6 | .typ { color: #606; } 7 | .lit { color: #066; } 8 | .pun { color: #660; } 9 | .pln { color: #000; } 10 | .tag { color: #008; } 11 | .atn { color: #606; } 12 | .atv { color: #080; } 13 | .dec { color: #606; } 14 | pre.prettyprint { padding: 2px; border: 1px solid #888 } 15 | 16 | /* Specify class=linenums on a pre to get line numbering */ 17 | ol.linenums { margin-top: 0; margin-bottom: 0 } /* IE indents via margin-left */ 18 | li.L0, 19 | li.L1, 20 | li.L2, 21 | li.L3, 22 | li.L5, 23 | li.L6, 24 | li.L7, 25 | li.L8 { list-style-type: none } 26 | /* Alternate shading for lines */ 27 | li.L1, 28 | li.L3, 29 | li.L5, 30 | li.L7, 31 | li.L9 { background: #eee } 32 | 33 | @media print { 34 | .str { color: #060; } 35 | .kwd { color: #006; font-weight: bold; } 36 | .com { color: #600; font-style: italic; } 37 | .typ { color: #404; font-weight: bold; } 38 | .lit { color: #044; } 39 | .pun { color: #440; } 40 | .pln { color: #000; } 41 | .tag { color: #006; font-weight: bold; } 42 | .atn { color: #404; } 43 | .atv { color: #060; } 44 | } 45 | -------------------------------------------------------------------------------- /test/browser/sparkbar.test.js: -------------------------------------------------------------------------------- 1 | module('Ico.SparkLine'); 2 | 3 | test('an inline sparkline', function() { 4 | var element = document.getElementById('sparkline'), 5 | sparkline = new Ico.SparkLine(element, 6 | [21, 41, 32, 1, 10, 5, 32, 10, 23], 7 | { width: 30, height: 14, background_colour: '#ccc' }); 8 | ok(sparkline); 9 | }); 10 | 11 | test('a sparkbar', function() { 12 | var element = document.getElementById('sparkline_2'), 13 | sparkbar = new Ico.SparkBar(element, 14 | [1, 5, 10, 15, 20, 15, 10, 15, 30, 15, 10], 15 | { width: 30, height: 14, background_colour: '#ccc' }); 16 | ok(sparkbar); 17 | }); 18 | 19 | test('a sparkline that needs highlights', function() { 20 | var element = document.getElementById('sparkline_3'), 21 | sparkline = new Ico.SparkLine(element, 22 | [10, 1, 12, 3, 4, 8, 5], 23 | { width: 60, height: 14, highlight: { colour: '#ff0000' }, 24 | acceptable_range: [5, 9], background_colour: '#ccc' }); 25 | ok(sparkline); 26 | }); 27 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h2. Introduction 2 | 3 | Ico is a JavaScript graph library that uses "Raphael":http://raphaeljs.com/ to draw. This means it can draw graphs in multiple browsers (including IE). 4 | 5 | Raphael is the only dependency. I've tested the examples with Raphael 2.0 and they appear to work correctly, but please report bugs through GitHub. 6 | 7 | * "Documentation":http://alexyoung.github.com/ico 8 | * "Examples":http://alexyoung.github.com/ico/examples.html 9 | 10 | h3. Support 11 | 12 | Donations to support future development are welcome and appreciated: "Donate":https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=support%40helicoid%2enet&lc=GB&item_name=Helicoid%20Limited&no_note=0&cn=Add%20special%20instructions%20to%20the%20seller&no_shipping=2¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted 13 | 14 | h3. Targeted graph types 15 | 16 | * Sparklines 17 | * Line graphs 18 | * Bar graphs 19 | 20 | h3. Design considerations and goals 21 | 22 | * Clarity: Use of white-space to help lend clarity to graphs, nominal scale vs. ordinal scale 23 | * Simplicity: Minimal use of decorations and lines, reliance on the Gestalt principle of closure 24 | * Conciseness: Avoidance of graph types that don't efficiently present data (pie charts, radar maps) 25 | 26 | These goals are based on recommendations in Stephen Few's books: 27 | 28 | * Show Me the Numbers: Designing Tables and Graphs to Enlighten 29 | * Information Dashboard Design 30 | 31 | Which was generally in turn based on Edward Tufte's work. 32 | 33 | h3. Test-Driven 34 | 35 | Built with QUnit and Node unit tests. 36 | 37 | h3. Todo 38 | 39 | * Humanize labels: rather than showing 1,000,000 optionally show "1m" 40 | 41 | h2. Examples 42 | 43 | See index.html for current API usage. This will change as I evolve the API to support the targeted graph types. 44 | 45 | h2. Requirements 46 | 47 | * "Raphael":http://raphaeljs.com 48 | 49 | h3. License 50 | 51 | MIT License. 52 | -------------------------------------------------------------------------------- /src/graphs/line.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Draws line graphs. 3 | * 4 | * Example: 5 | * 6 | * new Ico.LineGraph(element, [10, 5, 22, 44, 4]); 7 | * 8 | */ 9 | Ico.LineGraph = function() { this.initialize.apply(this, arguments); }; 10 | Helpers.extend(Ico.LineGraph.prototype, Ico.BaseGraph.prototype); 11 | Helpers.extend(Ico.LineGraph.prototype, { 12 | normalise: function(value) { 13 | if (value === 0) { 14 | return 0; 15 | } 16 | 17 | var total = this.start_value === 0 ? this.top_value : this.top_value - this.start_value; 18 | return ((value / total) * (this.graph_height)); 19 | }, 20 | 21 | chartDefaults: function() { 22 | return { plot_padding: 10, stroke_width: '3px' }; 23 | }, 24 | 25 | setChartSpecificOptions: function() { 26 | // Approximate the width required by the labels 27 | var longestLabel = this.longestLabel(this.value_labels); 28 | this.x_padding_left = 30 + longestLabel * (this.options.font_size / 2); 29 | 30 | if (typeof this.options.curve_amount === 'undefined') { 31 | this.options.curve_amount = 10; 32 | } 33 | }, 34 | 35 | normaliserOptions: function() { 36 | return { start_value: this.options.start_value }; 37 | }, 38 | 39 | calculateStep: function() { 40 | return this.graph_width / this.data_size; 41 | }, 42 | 43 | drawPlot: function(index, pathString, x, y, colour) { 44 | var w = this.options.curve_amount; 45 | 46 | if (this.options.markers === 'circle') { 47 | var circle = this.paper.circle(x, y, this.options.marker_size); 48 | circle.attr({ 'stroke-width': '1px', stroke: this.options.background_colour, fill: colour }); 49 | } 50 | 51 | if (index === 0) { 52 | this.lastPoint = { x: x, y: y }; 53 | return pathString + 'M' + x + ',' + y; 54 | } 55 | 56 | if (w) { 57 | pathString += ['C', this.lastPoint.x + w, this.lastPoint.y, x - w, y, x, y]; 58 | } else { 59 | pathString += 'L' + x + ',' + y; 60 | } 61 | 62 | this.lastPoint = { x: x, y: y }; 63 | return pathString; 64 | } 65 | }); 66 | 67 | -------------------------------------------------------------------------------- /src/base.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Ico 3 | * Copyright (C) 2009-2011 Alex R. Young 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * The Ico.Base object which contains useful generic functions. 9 | */ 10 | Ico.Base = { 11 | /** 12 | * Runs this.normalise on each value. 13 | * 14 | * @param {Array} data Values to normalise 15 | * @returns {Array} Normalised values 16 | */ 17 | normaliseData: function(data) { 18 | var values = [], 19 | i = 0; 20 | for (i = 0; i < data.length; i++) { 21 | values.push(this.normalise(data[i])); 22 | } 23 | return values; 24 | }, 25 | 26 | /** 27 | * Flattens objects into an array. 28 | * 29 | * @param {Object} data Values to flatten 30 | * @returns {Array} Flattened values 31 | */ 32 | flatten: function(data) { 33 | var flat_data = []; 34 | 35 | if (typeof data.length === 'undefined') { 36 | if (typeof data === 'object') { 37 | for (var key in data) { 38 | if (data.hasOwnProperty(key)) 39 | flat_data = flat_data.concat(this.flatten(data[key])); 40 | } 41 | } else { 42 | return []; 43 | } 44 | } 45 | 46 | for (var i = 0; i < data.length; i++) { 47 | if (typeof data[i].length === 'number') { 48 | flat_data = flat_data.concat(this.flatten(data[i])); 49 | } else { 50 | flat_data.push(data[i]); 51 | } 52 | } 53 | return flat_data; 54 | }, 55 | 56 | /** 57 | * Handy method to produce an array of numbers. 58 | * 59 | * @param {Integer} start A number to start at 60 | * @param {Integer} end A number to end at 61 | * @returns {Array} An array of values 62 | */ 63 | makeRange: function(start, end, options) { 64 | var values = [], i; 65 | for (i = start; i < end; i++) { 66 | if (options && options.skip) { 67 | if (i % options.skip === 0) { 68 | values.push(i); 69 | } else { 70 | values.push(undefined); 71 | } 72 | } else { 73 | values.push(i); 74 | } 75 | } 76 | return values; 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 0.3.10 / 2012-02-06 2 | ================== 3 | 4 | * Fixed issue 16: When defining labels, the vertical lines are incorrect 5 | 6 | 0.3.9 / 2012-02-06 7 | ================== 8 | 9 | * Added `stroke_width` option (defaults to `3px`, can be set to `0` to make scatter plots) 10 | * Clarified documentation 11 | * Added support for jQuery objects passed in as the graph's container element option 12 | * Improves handling of graphs with all zero values 13 | 14 | 0.3.8 / 2012-02-06 15 | ================== 16 | 17 | * Grouped `BarGraph` will now generate random colours correctly 18 | * Added line to `BarGraph` 19 | 20 | 0.3.7 / 2011-11-28 21 | ================== 22 | 23 | * If `labels` is manually supplied, the number of items will be used to determine the horizontal grid positions 24 | 25 | 0.3.6 / 2011-11-28 26 | ================== 27 | 28 | * Added grouped bar graphs, for documentation see [Grouped Bar Graphs](http://alexyoung.github.com/ico/) 29 | * Updated documentation to include details on each graph's options 30 | 31 | 0.3.5 / 2011-11-26 32 | ================== 33 | 34 | * Added `max_bar_width` option for controlling the maximum width a bar can be 35 | * Added `bar_padding` for controlling the padding between bars 36 | * Added `bar_width` for forcing a given bar width 37 | 38 | These options also apply to horizontal bar graphs. 39 | 40 | 0.3.4.1 / 2011-11-08 41 | ==================== 42 | 43 | * Added `bar_labels` option that displays values above the standard bar graphs 44 | 45 | 0.3.3 / 2011-09-30 46 | ================== 47 | 48 | * Added `label_step` and `label_count` options so numerical labels can be controlled 49 | 50 | 0.3.2 / 2011-08-30 51 | ================== 52 | 53 | * No longer extending native prototypes 54 | 55 | 0.3.1 / 2011-08-24 56 | ================== 57 | 58 | * Bug fix for LineGraph, migrated tests to QUnit, moved Jakefile.js to Makefile 59 | 60 | 0.3.0 / 2011-03-09 61 | ================== 62 | 63 | * Passing default JSHint, and a minified version is now available 64 | * Added documentation and published it to [alexyoung.github.com/ico/](http://alexyoung.github.com/ico/) 65 | * Split project into separate files, added build script, fixes for `HorizontalBarGraph` 66 | 67 | 0.2.2 / 2011-03-01 68 | ================== 69 | 70 | * `Ico.SparkLine` will now work correctly, particularly in IE6 71 | 72 | -------------------------------------------------------------------------------- /src/normaliser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Normalises lists of values to fit inside a graph. 3 | * 4 | * @param {Array} data A list of values 5 | * @param {Object} options Can be used to set the `start_value` 6 | */ 7 | Ico.Normaliser = function(data, options) { 8 | this.options = { 9 | start_value: null 10 | }; 11 | 12 | if (typeof options !== 'undefined') { 13 | this.options = options; 14 | } 15 | 16 | this.min = Helpers.min(data); 17 | this.max = this.options.max || Helpers.max(data); 18 | this.standard_deviation = Helpers.standard_deviation(data); 19 | this.range = 0; 20 | this.step = this.labelStep(this.max - this.min); 21 | this.start_value = this.calculateStart(); 22 | this.process(); 23 | }; 24 | 25 | Ico.Normaliser.prototype = { 26 | /** 27 | * Calculates the start value. This is often 0. 28 | * @returns {Float} The start value 29 | */ 30 | calculateStart: function() { 31 | var min = typeof this.options.start_value !== 'undefined' && this.min >= 0 ? this.options.start_value : this.min, 32 | start_value = this.round(min, 1); 33 | 34 | /* This is a boundary condition */ 35 | if (this.min > 0 && start_value > this.min) { 36 | return 0; 37 | } 38 | 39 | if (this.min === this.max) { 40 | return 0; 41 | } 42 | 43 | return start_value; 44 | }, 45 | 46 | /* Given a value, this method rounds it to the nearest good value for an origin */ 47 | round: function(value, offset) { 48 | offset = offset || 1; 49 | var roundedValue = value; 50 | 51 | if (this.standard_deviation > 0.1) { 52 | var multiplier = Math.pow(10, -offset); 53 | roundedValue = Math.round(value * multiplier) / multiplier; 54 | 55 | if (roundedValue > this.min) { 56 | return this.round(value - this.step); 57 | } 58 | } 59 | return roundedValue; 60 | }, 61 | 62 | /** 63 | * Calculates the range and step values. 64 | */ 65 | process: function() { 66 | this.range = this.max - this.start_value; 67 | this.step = this.labelStep(this.range); 68 | }, 69 | 70 | /** 71 | * Calculates the label step value. 72 | * 73 | * @param {Float} value A value to convert to a label position 74 | * @returns {Float} The rounded label step result 75 | */ 76 | labelStep: function(value) { 77 | return Math.pow(10, Math.round((Math.log(value) / Math.LN10)) - 1); 78 | } 79 | }; 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Determines if a value is valid as a 'step' value. 3 | * Steps are the increments between each bar or line. 4 | * 5 | * @param {Integer} value A number to test 6 | * @returns {Integer} A valid step value 7 | */ 8 | function validStepDivider(value) { 9 | return value > 1 ? value - 1 : 1; 10 | } 11 | 12 | /** 13 | * Gets a CSS style property. 14 | * 15 | * @param {Object} el A DOM element 16 | * @param {String} styleProp The name of a style property 17 | * @returns {Object} The style value 18 | */ 19 | function getStyle(el, styleProp) { 20 | if (typeof window === 'undefined') { 21 | return; 22 | } 23 | 24 | var style; 25 | if (el.currentStyle) { 26 | style = el.currentStyle[styleProp]; 27 | } else if (window.getComputedStyle) { 28 | style = document.defaultView.getComputedStyle(el, null).getPropertyValue(styleProp); 29 | } 30 | if (style && style.length === 0) { 31 | style = null; 32 | } 33 | return style; 34 | } 35 | 36 | var Helpers = {}; 37 | 38 | Helpers.sum = function(a) { 39 | var i, sum; 40 | for (i = 0, sum = 0; i < a.length; sum += a[i++]) {} 41 | return sum; 42 | }; 43 | 44 | if (typeof Array.prototype.max === 'undefined') { 45 | Helpers.max = function(a) { 46 | return Math.max.apply({}, a); 47 | }; 48 | } else { 49 | Helpers.max = function(a) { 50 | return a.max(); 51 | }; 52 | } 53 | 54 | if (typeof Array.prototype.min === 'undefined') { 55 | Helpers.min = function(a) { 56 | return Math.min.apply({}, a); 57 | }; 58 | } else { 59 | Helpers.min = function(a) { 60 | return a.min(); 61 | }; 62 | } 63 | 64 | Helpers.mean = function(a) { 65 | return Helpers.sum(a) / a.length; 66 | }; 67 | 68 | Helpers.variance = function(a) { 69 | var mean = Helpers.mean(a), 70 | variance = 0; 71 | for (var i = 0; i < a.length; i++) { 72 | variance += Math.pow(a[i] - mean, 2); 73 | } 74 | return variance / (a.length - 1); 75 | }; 76 | 77 | Helpers.standard_deviation = function(a) { 78 | return Math.sqrt(Helpers.variance(a)); 79 | }; 80 | 81 | if (typeof Object.extend === 'undefined') { 82 | Helpers.extend = function(destination, source) { 83 | for (var property in source) { 84 | if (source.hasOwnProperty(property)) { 85 | destination[property] = source[property]; 86 | } 87 | } 88 | return destination; 89 | }; 90 | } else { 91 | Helpers.extend = Object.extend; 92 | } 93 | 94 | if (Object.keys) { 95 | Helpers.keys = Object.keys; 96 | } else { 97 | Helpers.keys = function(o) { 98 | if (o !== Object(o)) { 99 | throw new TypeError('Object.keys called on non-object'); 100 | } 101 | 102 | var ret = [], p; 103 | for (p in o) { 104 | if (Object.prototype.hasOwnProperty.call(o,p)) { 105 | ret.push(p); 106 | } 107 | } 108 | return ret; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/graphs/sparkline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Draws spark line graphs. 3 | * 4 | * Example: 5 | * 6 | * new Ico.SparkLine(element, 7 | * [21, 41, 32, 1, 10, 5, 32, 10, 23], 8 | * { width: 30, height: 14, 9 | * background_colour: '#ccc' }); 10 | * 11 | * 12 | */ 13 | Ico.SparkLine = function() { this.initialize.apply(this, arguments); }; 14 | Ico.SparkLine.prototype = { 15 | initialize: function(element, data, options) { 16 | this.element = element; 17 | this.data = data; 18 | this.options = { 19 | width: parseInt(getStyle(element, 'width'), 10), 20 | height: parseInt(getStyle(element, 'height'), 10), 21 | highlight: false, 22 | background_colour: getStyle(element, 'backgroundColor') || '#ffffff', 23 | colour: '#036' 24 | }; 25 | Helpers.extend(this.options, options || { }); 26 | 27 | this.step = this.calculateStep(); 28 | this.paper = Raphael(this.element, this.options.width, this.options.height); 29 | 30 | if (this.options.acceptable_range) { 31 | this.background = this.paper.rect(0, this.options.height - this.normalise(this.options.acceptable_range[1]), 32 | this.options.width, 33 | this.options.height - this.normalise(this.options.acceptable_range[0])); 34 | } else { 35 | this.background = this.paper.rect(0, 0, this.options.width, this.options.height); 36 | } 37 | 38 | this.background.attr({fill: this.options.background_colour, stroke: 'none' }); 39 | this.draw(); 40 | }, 41 | 42 | calculateStep: function() { 43 | return this.options.width / validStepDivider(this.data.length); 44 | }, 45 | 46 | normalise: function(value) { 47 | return (this.options.height / Helpers.max(this.data)) * value; 48 | }, 49 | 50 | draw: function() { 51 | var data = this.normaliseData(this.data); 52 | this.drawLines('', this.options.colour, data); 53 | 54 | if (this.options.highlight) { 55 | this.showHighlight(data); 56 | } 57 | }, 58 | 59 | drawLines: function(label, colour, data) { 60 | var pathString = '', 61 | x = 0, 62 | values = data.slice(1), 63 | i = 0; 64 | 65 | pathString = 'M0,' + (this.options.height - data[0]); 66 | for (i = 1; i < data.length; i++) { 67 | x = x + this.step; 68 | pathString += 'L' + x +',' + Ico.round(this.options.height - data[i], 2); 69 | } 70 | this.paper.path(pathString).attr({stroke: colour}); 71 | this.lastPoint = { x: 0, y: this.options.height - data[0] }; 72 | }, 73 | 74 | showHighlight: function(data) { 75 | var size = 2, 76 | x = this.options.width - size, 77 | i = this.options.highlight.index || data.length - 1, 78 | y = data[i] + (Math.round(size / 2)); 79 | 80 | if (typeof(this.options.highlight.index) !== 'undefined') { 81 | x = this.step * this.options.highlight.index; 82 | } 83 | 84 | var circle = this.paper.circle(x, this.options.height - y, size); 85 | circle.attr({ stroke: false, fill: this.options.highlight.colour}); 86 | } 87 | }; 88 | Helpers.extend(Ico.SparkLine.prototype, Ico.Base); 89 | 90 | -------------------------------------------------------------------------------- /src/graphs/horizontal_bar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Draws horizontal bar graphs. 3 | * 4 | * Example: 5 | * 6 | * new Ico.HorizontalBarGraph(element, 7 | * [2, 5, 1, 10, 15, 33, 20, 25, 1], 8 | * { font_size: 14 }); 9 | * 10 | */ 11 | Ico.HorizontalBarGraph = function() { this.initialize.apply(this, arguments); }; 12 | Helpers.extend(Ico.HorizontalBarGraph.prototype, Ico.BaseGraph.prototype); 13 | Helpers.extend(Ico.HorizontalBarGraph.prototype, { 14 | setChartSpecificOptions: function() { 15 | // Approximate the width required by the labels 16 | this.y_padding_top = 0; 17 | this.x_padding_left = 20 + this.longestLabel() * (this.options.font_size / 2); 18 | this.bar_padding = this.options.bar_padding || 5; 19 | this.bar_width = this.options.bar_size || this.calculateBarHeight(); 20 | 21 | if (this.options.bar_size && !this.options.bar_padding) { 22 | this.bar_padding = this.graph_height / this.data_size; 23 | } 24 | 25 | this.options.plot_padding = 0; 26 | this.step = this.calculateStep(); 27 | }, 28 | 29 | normalise: function(value) { 30 | var offset = this.x_padding_left; 31 | return ((value / this.range) * (this.graph_width - offset)); 32 | }, 33 | 34 | /* Height */ 35 | calculateBarHeight: function() { 36 | var height = (this.graph_height / this.data_size) - this.bar_padding; 37 | 38 | if (this.options.max_bar_size && height > this.options.max_bar_size) { 39 | height = this.options.max_bar_size; 40 | this.bar_padding = this.graph_height / this.data_size; 41 | } 42 | 43 | return height; 44 | }, 45 | 46 | calculateStep: function() { 47 | return (this.options.height - this.y_padding_bottom) / validStepDivider(this.data_size); 48 | }, 49 | 50 | drawLines: function(label, colour, data) { 51 | var x = this.x_padding_left + (this.options.plot_padding * 2), 52 | y = this.options.height - this.y_padding_bottom - (this.step / 2), 53 | pathString = 'M' + x + ',' + y, 54 | i; 55 | 56 | for (i = 0; i < data.length; i++) { 57 | pathString += 'L' + (x + data[i] - this.normalise(this.start_value)) + ',' + y; 58 | y = y - this.step; 59 | pathString += 'M' + x + ',' + y; 60 | } 61 | this.paper.path(pathString).attr({stroke: colour, 'stroke-width': this.bar_width + 'px'}); 62 | }, 63 | 64 | /* Horizontal version */ 65 | drawFocusHint: function() { 66 | var length = 5, 67 | x = this.x_padding_left + (this.step * 2), 68 | y = this.options.height - this.y_padding_bottom, 69 | pathString = ''; 70 | 71 | pathString += 'M' + x + ',' + y; 72 | pathString += 'L' + (x - length) + ',' + (y + length); 73 | pathString += 'M' + (x - length) + ',' + y; 74 | pathString += 'L' + (x - (length * 2)) + ',' + (y + length); 75 | this.paper.path(pathString).attr({stroke: this.options.label_colour, 'stroke-width': 2}); 76 | }, 77 | 78 | drawVerticalLabels: function() { 79 | var y_start = (this.step / 2) - (this.options.plot_padding * 2); 80 | this.drawMarkers(this.options.labels, [0, -1], this.step, y_start, [-8, (this.options.font_size / 8)], { 'text-anchor': 'end' }); 81 | }, 82 | 83 | drawHorizontalLabels: function() { 84 | var x_step = this.graph_width / this.y_label_count, 85 | x_labels = this.makeValueLabels(this.y_label_count); 86 | this.drawMarkers(x_labels, [1, 0], x_step, x_step, [0, (this.options.font_size + 7) * -1]); 87 | } 88 | }); 89 | 90 | -------------------------------------------------------------------------------- /test/browser/normalisation.test.js: -------------------------------------------------------------------------------- 1 | module('Normalisation'); 2 | 3 | test('test_0s', function() { 4 | var normaliser = new Ico.Normaliser([0, 0, 0, 0, 0]); 5 | equal(0, normaliser.step); 6 | equal(0, normaliser.start_value); 7 | }); 8 | 9 | test('test_0_to_1', function() { 10 | var normaliser = new Ico.Normaliser([0.1, 0.5, 0.9, 1.0]); 11 | equal(0.1, normaliser.step); 12 | equal(0.0, normaliser.start_value); 13 | }); 14 | 15 | test('test_really_small', function() { 16 | var normaliser = new Ico.Normaliser([0.9999, 0.994, 0.93, 0.92]); 17 | equal(0.1, Ico.round(normaliser.step, 2)); 18 | }); 19 | 20 | test('test_1095_to_1100', function() { 21 | /* Specifying the start_value is done by bar graphs -- they don't really make sense with non-zero origins */ 22 | var normaliser = new Ico.Normaliser([1095, 1099, 1100, 1096], { start_value: 0 }); 23 | equal(100, normaliser.step); 24 | equal(0, normaliser.start_value); 25 | }); 26 | 27 | test('test_1_to_33', function() { 28 | var normaliser = new Ico.Normaliser([10, 9, 3, 30, 1, 10, 5, 33, 33]); 29 | equal(10, normaliser.step); 30 | }); 31 | 32 | test('test_negative_large_range', function() { 33 | var normaliser = new Ico.Normaliser([-57,-31,-87,66,-30,-77,-88,-75,-20,-48,-56,-91,16,-41,-87,-69,-65,-62,58,-15,-49,-75,-42,-78,-79]); 34 | equal(10, normaliser.step); 35 | equal(-91, normaliser.min); 36 | 37 | /* The start value should round down to the nearest readable value */ 38 | equal(-100, normaliser.start_value); 39 | }); 40 | 41 | test('test_negative_medium_range', function() { 42 | var normaliser = new Ico.Normaliser([10, 10, 10, 10, 10, 10, 5, 6, 9, 11, 14, -25]); 43 | equal(10, normaliser.step); 44 | equal(-25, normaliser.min); 45 | equal(-30, normaliser.start_value); 46 | }); 47 | 48 | test('test_90_100', function() { 49 | var normaliser = new Ico.Normaliser([90, 95, 100], { start_value: 0 }); 50 | equal(10, normaliser.step); 51 | equal(0, normaliser.start_value); 52 | }); 53 | 54 | test('test_same_values', function() { 55 | var normaliser = new Ico.Normaliser([20, 20], { start_value: 0 }); 56 | equal(1, normaliser.step); 57 | equal(20, normaliser.range); 58 | equal(0, normaliser.start_value); 59 | }); 60 | 61 | test('test_19_20', function() { 62 | var normaliser = new Ico.Normaliser([19, 20], { start_value: 0 }); 63 | equal(0, normaliser.start_value); 64 | equal(1, normaliser.step); 65 | }); 66 | 67 | test('test_negative_values', function() { 68 | /* Negative values are the only case where start_value should be ignored */ 69 | var normaliser = new Ico.Normaliser([-10, 1, 20], { start_value: 0 }); 70 | equal(-10, normaliser.start_value); 71 | }); 72 | 73 | test('test_rre_bug', function() { 74 | var normaliser = new Ico.Normaliser([10, 10, 10, 5, 5, 5]); 75 | equal(1, normaliser.step); 76 | equal(0, normaliser.start_value); 77 | }); 78 | 79 | test('test_max_has_headrom', function() { 80 | var normaliser = new Ico.Normaliser([30, 5, 1, 10, 15, 18, 20, 25, 1, 10, 9, 3, 30, 1, 10, 5, 33, 33, 5, 4, 10, 1, 30, 11, 33, 12, 22]); 81 | }); 82 | 83 | test('A set of the same values gives a sensible start_value', function() { 84 | var normaliser = new Ico.Normaliser([20, 20, 20, 20, 20]); 85 | equal(0, normaliser.start_value); 86 | }); 87 | 88 | test('test_normalisation_floats', function() { 89 | var normaliser = new Ico.Normaliser([100.3, 100.4, 101.3, 100.4, 101.2, 101.8, 102.0, 103.5, 103.7, 103.1, 104.1, 103.0, 102.6, 104.2, 104.1, 103.3, 103.9, 104.6, 103.4, 104.5, 103.5, 103.6, 104.6, 104.4, 104.5, 103.7, 103.8, 102.9, 102.5, 102.3, 101.8, 103.1, 102.0, 100.8, 100.4, 100.3, 100.7, 100.7, 101.3, 101.6, 102.6, 98.0, 100.1, 100.8, 100.7, 100.3, 100.1, 100.9, 99.2, 100.1, 99.8, 99.9, 99.8, 99.3, 100.1, 100.2, 99.5, 99.8, 99.7, 100.8, 100.7, 100.0, 101.2, 101.2, 100.7, 101.3, 102.0, 101.6, 101.4, 101.1, 101.4, 100.3, 100.2, 100.6, 99.8, 100.0, 101.1, 100.6, 100.8, 100.3, 100.2, 100.7, 99.6, 100.2, 100.4, 100.4, 100.5, 100.3, 99.6, 99.5, 99.1, 98.3, 99.0, 99.1, 99.6, 100.2, 100.6, 100.3, 101.2, 100.0, 100.5, 100.4, 100.6, 100.1, 100.7, 100.7, 101.2, 100.3, 100.6, 100.3, 100.1, 100.5, 100.2, 99.8, 100.3, 100.5, 101.1, 101.5, 101.5, 101.2, 101.1, 100.9, 100.8, 101.4, 101.2, 101.3, 101.2, 100.9, 101.0, 100.6, 100.6, 99.9, 99.8, 99.5, 98.8, 98.6]); 90 | equal(10, normaliser.step); 91 | }); 92 | -------------------------------------------------------------------------------- /docs/layout/begin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | alexyoung/ico @ GitHub 6 | 7 | 8 | 87 | 88 | 89 | Fork me on GitHub 90 | 91 |
    92 | 93 |
    94 | 95 | 96 | 97 | 98 |
    99 | 100 |

    ico 101 | by alexyoung

    102 | 103 | -------------------------------------------------------------------------------- /src/graphs/bar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The BarGraph class. 3 | * 4 | * Example: 5 | * 6 | * new Ico.BarGraph($('bargraph'), [100, 10, 90, 20, 80, 30]); 7 | * 8 | */ 9 | Ico.BarGraph = function() { this.initialize.apply(this, arguments); }; 10 | Helpers.extend(Ico.BarGraph.prototype, Ico.BaseGraph.prototype); 11 | Helpers.extend(Ico.BarGraph.prototype, { 12 | // Overridden to handle grouped bar graphs 13 | buildDataSets: function(data, options) { 14 | if (typeof data.length !== 'undefined') { 15 | if (typeof data[0].length !== 'undefined') { 16 | this.grouped = true; 17 | 18 | // TODO: Find longest? 19 | this.group_size = data[0].length; 20 | var o = {}, k, i = 0; 21 | for (k in options.labels) { 22 | k = options.labels[k]; 23 | o[k] = data[i]; 24 | i++; 25 | } 26 | return o; 27 | } else { 28 | return { 'one': data }; 29 | } 30 | } else { 31 | return data; 32 | } 33 | }, 34 | 35 | /** 36 | * Sensible defaults for BarGraph. 37 | */ 38 | chartDefaults: function() { 39 | return { plot_padding: 0 }; 40 | }, 41 | 42 | /** 43 | * Ensures the normalises is always 0. 44 | */ 45 | normaliserOptions: function() { 46 | // Make sure the true largest value is used for max 47 | return this.options.line ? { start_value: 0, max: Helpers.max([Helpers.max(this.options.line), Helpers.max(this.flat_data)]) } : { start_value: 0 }; 48 | }, 49 | 50 | /** 51 | * Options specific to BarGraph. 52 | */ 53 | setChartSpecificOptions: function() { 54 | this.bar_padding = this.options.bar_padding || 5; 55 | this.bar_width = this.options.bar_size || this.calculateBarWidth(); 56 | 57 | if (this.options.bar_size && !this.options.bar_padding) { 58 | this.bar_padding = this.graph_width / this.data_size; 59 | } 60 | 61 | this.options.plot_padding = (this.bar_width / 2) - (this.bar_padding / 2); 62 | this.step = this.calculateStep(); 63 | this.grid_start_offset = this.bar_padding - 1; 64 | this.start_y = this.options.height - this.y_padding_bottom; 65 | }, 66 | 67 | /** 68 | * Calculates the width of each bar. 69 | * 70 | * @returns {Integer} The bar width 71 | */ 72 | calculateBarWidth: function() { 73 | var width = (this.graph_width / this.data_size) - this.bar_padding; 74 | 75 | if (this.grouped) { 76 | //width = width / this.group_size - (this.bar_padding * this.group_size); 77 | } 78 | 79 | if (this.options.max_bar_size && width > this.options.max_bar_size) { 80 | width = this.options.max_bar_size; 81 | this.bar_padding = this.graph_width / this.data_size; 82 | } 83 | 84 | return width; 85 | }, 86 | 87 | /** 88 | * Calculates step used to move from one bar to another. 89 | * 90 | * @returns {Float} The start value 91 | */ 92 | calculateStep: function() { 93 | return (this.graph_width - (this.options.plot_padding * 2) - (this.bar_padding * 2)) / validStepDivider(this.data_size); 94 | }, 95 | 96 | /** 97 | * Generates paths for Raphael. 98 | * 99 | * @param {Integer} index The index of the data value to plot 100 | * @param {String} pathString The pathString so far 101 | * @param {Integer} x The x-coord to plot 102 | * @param {Integer} y The y-coord to plot 103 | * @param {String} colour A string that represents a colour 104 | * @returns {String} The resulting path string 105 | */ 106 | drawPlot: function(index, pathString, x, y, colour) { 107 | if (this.options.highlight_colours && this.options.highlight_colours.hasOwnProperty(index)) { 108 | colour = this.options.highlight_colours[index]; 109 | } 110 | 111 | x = x + this.bar_padding; 112 | pathString += 'M' + x + ',' + this.start_y; 113 | pathString += 'L' + x + ',' + y; 114 | this.paper.path(pathString).attr({ stroke: colour, 'stroke-width': this.bar_width + 'px' }); 115 | pathString = ''; 116 | x = x + this.step; 117 | pathString += 'M' + x + ',' + this.start_y; 118 | return pathString; 119 | }, 120 | 121 | /* Change the standard options to correctly offset against the bars */ 122 | drawHorizontalLabels: function() { 123 | var x_start = this.bar_padding + this.options.plot_padding, 124 | step = this.step; 125 | if (this.grouped) { 126 | step = step * this.group_size; 127 | x_start = ((this.bar_width * this.group_size) + (this.bar_padding * this.group_size)) / 2 128 | x_start = this.roundValue(x_start, 0); 129 | } 130 | this.drawMarkers(this.options.labels, [1, 0], step, x_start, [0, (this.options.font_size + 7) * -1]); 131 | }, 132 | 133 | drawBarMarkers: function() { 134 | if (this.plottedCoords.length === 0) { 135 | return; 136 | } 137 | 138 | var i, length = this.flat_data.length, x, y, label, font_options = {}; 139 | Helpers.extend(font_options, this.font_options); 140 | font_options['text-anchor'] = 'center'; 141 | 142 | for (i = 0; i < length; i++) { 143 | label = this.roundValue(this.flat_data[i], 2).toString(); 144 | x = this.plottedCoords[i][0]; 145 | y = this.roundValue(this.plottedCoords[i][1], 0); 146 | this.paper.text(x, y - this.options.font_size, label).attr(font_options).toFront(); 147 | } 148 | } 149 | }); 150 | 151 | -------------------------------------------------------------------------------- /test/browser/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-banner { 58 | height: 5px; 59 | } 60 | 61 | #qunit-testrunner-toolbar { 62 | padding: 0.5em 0 0.5em 2em; 63 | color: #5E740B; 64 | background-color: #eee; 65 | } 66 | 67 | #qunit-userAgent { 68 | padding: 0.5em 0 0.5em 2.5em; 69 | background-color: #2b81af; 70 | color: #fff; 71 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 72 | } 73 | 74 | 75 | /** Tests: Pass/Fail */ 76 | 77 | #qunit-tests { 78 | list-style-position: inside; 79 | } 80 | 81 | #qunit-tests li { 82 | padding: 0.4em 0.5em 0.4em 2.5em; 83 | border-bottom: 1px solid #fff; 84 | list-style-position: inside; 85 | } 86 | 87 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 88 | display: none; 89 | } 90 | 91 | #qunit-tests li strong { 92 | cursor: pointer; 93 | } 94 | 95 | #qunit-tests li a { 96 | padding: 0.5em; 97 | color: #c2ccd1; 98 | text-decoration: none; 99 | } 100 | #qunit-tests li a:hover, 101 | #qunit-tests li a:focus { 102 | color: #000; 103 | } 104 | 105 | #qunit-tests ol { 106 | margin-top: 0.5em; 107 | padding: 0.5em; 108 | 109 | background-color: #fff; 110 | 111 | border-radius: 15px; 112 | -moz-border-radius: 15px; 113 | -webkit-border-radius: 15px; 114 | 115 | box-shadow: inset 0px 2px 13px #999; 116 | -moz-box-shadow: inset 0px 2px 13px #999; 117 | -webkit-box-shadow: inset 0px 2px 13px #999; 118 | } 119 | 120 | #qunit-tests table { 121 | border-collapse: collapse; 122 | margin-top: .2em; 123 | } 124 | 125 | #qunit-tests th { 126 | text-align: right; 127 | vertical-align: top; 128 | padding: 0 .5em 0 0; 129 | } 130 | 131 | #qunit-tests td { 132 | vertical-align: top; 133 | } 134 | 135 | #qunit-tests pre { 136 | margin: 0; 137 | white-space: pre-wrap; 138 | word-wrap: break-word; 139 | } 140 | 141 | #qunit-tests del { 142 | background-color: #e0f2be; 143 | color: #374e0c; 144 | text-decoration: none; 145 | } 146 | 147 | #qunit-tests ins { 148 | background-color: #ffcaca; 149 | color: #500; 150 | text-decoration: none; 151 | } 152 | 153 | /*** Test Counts */ 154 | 155 | #qunit-tests b.counts { color: black; } 156 | #qunit-tests b.passed { color: #5E740B; } 157 | #qunit-tests b.failed { color: #710909; } 158 | 159 | #qunit-tests li li { 160 | margin: 0.5em; 161 | padding: 0.4em 0.5em 0.4em 0.5em; 162 | background-color: #fff; 163 | border-bottom: none; 164 | list-style-position: inside; 165 | } 166 | 167 | /*** Passing Styles */ 168 | 169 | #qunit-tests li li.pass { 170 | color: #5E740B; 171 | background-color: #fff; 172 | border-left: 26px solid #C6E746; 173 | } 174 | 175 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 176 | #qunit-tests .pass .test-name { color: #366097; } 177 | 178 | #qunit-tests .pass .test-actual, 179 | #qunit-tests .pass .test-expected { color: #999999; } 180 | 181 | #qunit-banner.qunit-pass { background-color: #C6E746; } 182 | 183 | /*** Failing Styles */ 184 | 185 | #qunit-tests li li.fail { 186 | color: #710909; 187 | background-color: #fff; 188 | border-left: 26px solid #EE5757; 189 | white-space: pre; 190 | } 191 | 192 | #qunit-tests > li:last-child { 193 | border-radius: 0 0 15px 15px; 194 | -moz-border-radius: 0 0 15px 15px; 195 | -webkit-border-bottom-right-radius: 15px; 196 | -webkit-border-bottom-left-radius: 15px; 197 | } 198 | 199 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 200 | #qunit-tests .fail .test-name, 201 | #qunit-tests .fail .module-name { color: #000000; } 202 | 203 | #qunit-tests .fail .test-actual { color: #EE5757; } 204 | #qunit-tests .fail .test-expected { color: green; } 205 | 206 | #qunit-banner.qunit-fail { background-color: #EE5757; } 207 | 208 | 209 | /** Result */ 210 | 211 | #qunit-testresult { 212 | padding: 0.5em 0.5em 0.5em 2.5em; 213 | 214 | color: #2b81af; 215 | background-color: #D2E0E6; 216 | 217 | border-bottom: 1px solid white; 218 | } 219 | 220 | /** Fixture */ 221 | 222 | #qunit-fixture { 223 | position: absolute; 224 | top: -10000px; 225 | left: -10000px; 226 | } 227 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ### Ico 2 | 3 | Ico (GitHub: [alexyoung / ico](https://github.com/alexyoung/ico), License: _MIT_) is a JavaScript graph library. 4 | 5 | The [Ico Example Page](http://alexyoung.github.com/ico/examples.html) has a lot of examples that demonstrate Ico's usage. 6 | 7 | ### Usage 8 | 9 | Graphs are created by instantiated classes. The available classes are: 10 | 11 | * `Ico.SparkLine`: Creates a small line graph intended for use within text 12 | * `Ico.SparkBar`: Creates a small bar graph intended for use within text 13 | * `Ico.BarGraph`: Creates a bar graph 14 | * `Ico.HorizontalBarGraph`: Creates a horizontal bar graph 15 | * `Ico.LineGraph`: Creates a line graph 16 | 17 | Each of these classes can be instantiated with the same arguments. The following examples use jQuery, but jQuery is not required to use Ico. 18 | 19 |
    
     20 | // Basic signature
     21 | new Ico.SparkLine(element, data, options);
     22 | 
     23 | // Real examples
     24 | new Ico.SparkLine($('#sparkline'),
     25 |   [21, 41, 32, 1, 10, 5, 32, 10, 23],
     26 |   { width: 30, height: 14, background_colour: '#ccc' }
     27 | );
     28 | 
     29 | new Ico.BarGraph($('#bargraph'), { one: [44, 12, 17, 30, 11] }, { bar_labels: true });
     30 | 
    31 | 32 | The third argument, `options`, may vary between graphs. 33 | 34 | ### Live Example 35 | 36 |
    
     37 | new Ico.LineGraph($('#linegraph'), {
     38 |     one: [30, 5, 1, 10, 15, 18, 20, 25, 1],
     39 |     two: [10, 9, 3, 30, 1, 10, 5, 33, 33],
     40 |     three: [5, 4, 10, 1, 30, 11, 33, 12, 22]
     41 |   }, {
     42 |     markers: 'circle',
     43 |     colours: { one: '#990000', two: '#009900', three: '#000099'},
     44 |     labels: ['one', 'two', 'three', 'four',
     45 |              'five', 'six', 'seven', 'eight', 'nine'],
     46 |     meanline: true,
     47 |     grid: true
     48 |   }
     49 | );
     50 | 
    51 | 52 | 53 | 54 |
    55 | 70 | 71 | ### Options for `Ico.SparkLine` 72 | 73 | * `width`: Width of the graph, defaults to the element's width 74 | * `height`: Height of the graph, defaults to the element's height 75 | * `highlight`: Highlight options `highlight: { colour: '#ff0000' }` -- used to pick out the last value 76 | * `background_colour`: The graph's background colour, defaults to the element's background colour if set 77 | * `colour`: The colour for drawing lines 78 | * `acceptable_range`: An array of two values, `[min, max]`, for setting the size of the background rectangle 79 | 80 | ### Options for `Ico.SparkBar` 81 | 82 | `Ico.SparkBar` options are the same as `Ico.SparkLine`. 83 | 84 | ### Shared Options for `Ico.BarGraph`, `Ico.HorizontalBarGraph`, and `Ico.LineGraph` 85 | 86 | * `width`: The width of the container element, defaults to the element's width 87 | * `height`: The height of the container element, defaults to the element's height 88 | * `background_colour`: The graph's background colour, defaults to the element's background colour if set 89 | * `labels`: An array of text labels (for each bar or line) 90 | * `show_horizontal_labels`: Set to `false` to hide horizontal labels 91 | * `show_vertical_labels`: Set to `false` to hide vertical labels 92 | * `label_count`: The number of numerical labels to display 93 | * `label_step`: The value to increment each numerical label 94 | * `start_value`: The value to start plotting from (generally 0). This can be used to force 0 in cases where the normaliser starts from another value 95 | * `font_size`: The size of the fonts used in the graph 96 | * `meanline`: Display a line through the mean value 97 | * `grid`: Display a grid to make reading values easier 98 | * `grid_colour`: Change the colour of the grid 99 | 100 | ### Options for `Ico.BarGraph` 101 | 102 | * `colour`: The colour for the bars 103 | * `colours`: An array of colours for each bar 104 | * `highlight_colours`: An object with the index of a bar (starting from 0) and a colour, like this `{ 3: '#ff0000' }` 105 | * `bar_size`: Set the size for a bar in a bar graph 106 | * `max_bar_size`: Set the maximum size for a bar in a bar graph 107 | * `bar_labels`: Display the actual value of each bar in a bar graph 108 | * `line`: Provide an array to plot a line alongside a bar graph 109 | 110 | ### Options for `Ico.LineGraph` 111 | 112 | * `stroke_width`: Sets the stroke width, defaults to `3px`. Set to `0` to get a scatter plot 113 | 114 | ### Grouped Bar Graphs 115 | 116 | Multidimensional arrays will be rendered as 'grouped' bar graphs. Notice that two colours are specified, one for each bar in the group. This is still a work in progress and hasn't been tested thoroughly yet. 117 | 118 | In grouped bar graphs, the index for `highlight_colours` is the index from left to right, starting from zero. 119 | 120 |
    
    121 | new Ico.BarGraph(
    122 |   $('grouped_bars'),
    123 |   [[10, 15], [18, 19], [17, 23], [11, 22]],
    124 |   { grid: true, font_size: 10,
    125 |     colours: ['#ff0099', '#339933'],
    126 |     labels: ['Winter', 'Spring', 'Summer', 'Autumn']
    127 |   }
    128 | );
    129 | 
    130 | 131 |
    132 | 142 | 143 | ### Options for `Ico.HorizontalBarGraph` 144 | 145 | * `bar_size`: Set the size for a bar in a bar graph 146 | * `max_bar_size`: Set the maximum size for a bar in a bar graph 147 | 148 | ### Options for `Ico.LineGraph` 149 | 150 | * `markers`: Set to `'circle'` to display markers at each point on a line graph 151 | * `marker_size`: The size of each marker 152 | 153 | ### Data Normalisation 154 | 155 | Data is mapped to plottable values by `Ico.Normaliser`. In addition, this class attempts to calculate a sensible value to start plotting from on the X axis, and also calculates the width for each item (the "step"). 156 | 157 | These values can be overridden by setting `start_value` and `label_step`. This can help display data that is difficult to plot, but you can raise issues through GitHub to report such data. 158 | 159 | -------------------------------------------------------------------------------- /docs/examples.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | Raphael Ico 6 | 7 | 8 | 33 | 52 | 53 | 54 |

    Sparklines

    55 |

    Sparklines are tiny graphs that you can use inline. You can use more than one in a document.

    56 | 57 |

    Sometimes it's useful to highlight a value in a sparkline:

    58 | 59 |

    Today there were 5 daily defects.

    60 | 61 |

    That example also showed a range using a background colour. This could represent a set of acceptable values.

    62 | 63 |

    Bar Graph

    64 |
    65 |
    66 |
    67 | 68 |

    Line Graph

    69 |
    70 |
    71 |
    72 |
    73 |
    74 |
    75 | 76 |

    Horizontal Bar Graph

    77 |
    78 |
    79 |
    80 |
    81 | 82 |

    Focused Data Ranges

    83 | 84 |

    Centre onto awkward data ranges:

    85 | 86 |
    87 |
    88 |
    89 |
    90 |
    91 |
    92 | 93 |

    Grouped Bars

    94 | 95 |
    96 |
    97 | 98 |

    Contributed Examples

    99 | 100 |

    Min and max are the same:

    101 |
    102 | 103 |

    Small range difference, but should still start at 0:

    104 |
    105 | 106 |

    Small range difference, but should still start at 0 (it's a bar chart):

    107 |
    108 | 109 |

    Large min max set to the same:

    110 |
    111 | 112 |

    Negative values should always be above the axis (not fixed yet):

    113 |
    114 | 115 |

    GitHub issue #4: LineGraph with the same values:

    116 |
    117 | 118 |

    TODO: Automatically reduce numerically labels when there are too many to neatly fit.

    119 |
    120 | 121 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | alexyoung/ico @ GitHub 6 | 7 | 8 | 87 | 88 | 89 | Fork me on GitHub 90 | 91 |
    92 | 93 |
    94 | 95 | 96 | 97 | 98 |
    99 | 100 |

    ico 101 | by alexyoung

    102 | 103 |

    Ico

    104 | 105 |

    Ico (GitHub: alexyoung / ico, License: MIT) is a JavaScript graph library.

    106 | 107 |

    The Ico Example Page has a lot of examples that demonstrate Ico's usage.

    108 | 109 |

    Usage

    110 | 111 |

    Graphs are created by instantiated classes. The available classes are:

    112 | 113 |
      114 |
    • Ico.SparkLine: Creates a small line graph intended for use within text
    • 115 |
    • Ico.SparkBar: Creates a small bar graph intended for use within text
    • 116 |
    • Ico.BarGraph: Creates a bar graph
    • 117 |
    • Ico.HorizontalBarGraph: Creates a horizontal bar graph
    • 118 |
    • Ico.LineGraph: Creates a line graph
    • 119 |
    120 | 121 |

    Each of these classes can be instantiated with the same arguments. The following examples use jQuery, but jQuery is not required to use Ico.

    122 | 123 |
    
    124 | // Basic signature
    125 | new Ico.SparkLine(element, data, options);
    126 | 
    127 | // Real examples
    128 | new Ico.SparkLine($('#sparkline'),
    129 |   [21, 41, 32, 1, 10, 5, 32, 10, 23],
    130 |   { width: 30, height: 14, background_colour: '#ccc' }
    131 | );
    132 | 
    133 | new Ico.BarGraph($('#bargraph'), { one: [44, 12, 17, 30, 11] }, { bar_labels: true });
    134 | 
    135 | 136 |

    The third argument, options, may vary between graphs.

    137 | 138 |

    Live Example

    139 | 140 |
    
    141 | new Ico.LineGraph($('#linegraph'), {
    142 |     one: [30, 5, 1, 10, 15, 18, 20, 25, 1],
    143 |     two: [10, 9, 3, 30, 1, 10, 5, 33, 33],
    144 |     three: [5, 4, 10, 1, 30, 11, 33, 12, 22]
    145 |   }, {
    146 |     markers: 'circle',
    147 |     colours: { one: '#990000', two: '#009900', three: '#000099'},
    148 |     labels: ['one', 'two', 'three', 'four',
    149 |              'five', 'six', 'seven', 'eight', 'nine'],
    150 |     meanline: true,
    151 |     grid: true
    152 |   }
    153 | );
    154 | 
    155 | 156 | 157 | 158 |
    159 | 174 | 175 |

    Options for Ico.SparkLine

    176 | 177 |
      178 |
    • width: Width of the graph, defaults to the element's width
    • 179 |
    • height: Height of the graph, defaults to the element's height
    • 180 |
    • highlight: Highlight options highlight: { colour: '#ff0000' } -- used to pick out the last value
    • 181 |
    • background_colour: The graph's background colour, defaults to the element's background colour if set
    • 182 |
    • colour: The colour for drawing lines
    • 183 |
    • acceptable_range: An array of two values, [min, max], for setting the size of the background rectangle
    • 184 |
    185 | 186 |

    Options for Ico.SparkBar

    187 | 188 |

    Ico.SparkBar options are the same as Ico.SparkLine.

    189 | 190 |

    Shared Options for Ico.BarGraph, Ico.HorizontalBarGraph, and Ico.LineGraph

    191 | 192 |
      193 |
    • width: The width of the container element, defaults to the element's width
    • 194 |
    • height: The height of the container element, defaults to the element's height
    • 195 |
    • background_colour: The graph's background colour, defaults to the element's background colour if set
    • 196 |
    • labels: An array of text labels (for each bar or line)
    • 197 |
    • show_horizontal_labels: Set to false to hide horizontal labels
    • 198 |
    • show_vertical_labels: Set to false to hide vertical labels
    • 199 |
    • label_count: The number of numerical labels to display
    • 200 |
    • label_step: The value to increment each numerical label
    • 201 |
    • start_value: The value to start plotting from (generally 0). This can be used to force 0 in cases where the normaliser starts from another value
    • 202 |
    • font_size: The size of the fonts used in the graph
    • 203 |
    • meanline: Display a line through the mean value
    • 204 |
    • grid: Display a grid to make reading values easier
    • 205 |
    • grid_colour: Change the colour of the grid
    • 206 |
    207 | 208 |

    Options for Ico.BarGraph

    209 | 210 |
      211 |
    • colour: The colour for the bars
    • 212 |
    • colours: An array of colours for each bar
    • 213 |
    • highlight_colours: An object with the index of a bar (starting from 0) and a colour, like this { 3: '#ff0000' }
    • 214 |
    • bar_size: Set the size for a bar in a bar graph
    • 215 |
    • max_bar_size: Set the maximum size for a bar in a bar graph
    • 216 |
    • bar_labels: Display the actual value of each bar in a bar graph
    • 217 |
    • line: Provide an array to plot a line alongside a bar graph
    • 218 |
    219 | 220 |

    Options for Ico.LineGraph

    221 | 222 |
      223 |
    • stroke_width: Sets the stroke width, defaults to 3px. Set to 0 to get a scatter plot
    • 224 |
    225 | 226 |

    Grouped Bar Graphs

    227 | 228 |

    Multidimensional arrays will be rendered as 'grouped' bar graphs. Notice that two colours are specified, one for each bar in the group. This is still a work in progress and hasn't been tested thoroughly yet.

    229 | 230 |

    In grouped bar graphs, the index for highlight_colours is the index from left to right, starting from zero.

    231 | 232 |
    
    233 | new Ico.BarGraph(
    234 |   $('grouped_bars'),
    235 |   [[10, 15], [18, 19], [17, 23], [11, 22]],
    236 |   { grid: true, font_size: 10,
    237 |     colours: ['#ff0099', '#339933'],
    238 |     labels: ['Winter', 'Spring', 'Summer', 'Autumn']
    239 |   }
    240 | );
    241 | 
    242 | 243 |
    244 | 245 | 255 | 256 |

    Options for Ico.HorizontalBarGraph

    257 | 258 |
      259 |
    • bar_size: Set the size for a bar in a bar graph
    • 260 |
    • max_bar_size: Set the maximum size for a bar in a bar graph
    • 261 |
    262 | 263 |

    Options for Ico.LineGraph

    264 | 265 |
      266 |
    • markers: Set to 'circle' to display markers at each point on a line graph
    • 267 |
    • marker_size: The size of each marker
    • 268 |
    269 | 270 |

    Data Normalisation

    271 | 272 |

    Data is mapped to plottable values by Ico.Normaliser. In addition, this class attempts to calculate a sensible value to start plotting from on the X axis, and also calculates the width for each item (the "step").

    273 | 274 |

    These values can be overridden by setting start_value and label_step. This can help display data that is difficult to plot, but you can raise issues through GitHub to report such data.

    275 | 276 | 277 | -------------------------------------------------------------------------------- /src/graphs/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ico.BaseGraph is extended by most of the other graphs. It 3 | * uses a simple pattern with methods that can be overridden. 4 | */ 5 | Ico.BaseGraph = function() { this.initialize.apply(this, arguments); }; 6 | Helpers.extend(Ico.BaseGraph.prototype, Ico.Base); 7 | Helpers.extend(Ico.BaseGraph.prototype, { 8 | /** 9 | * This base class is used by the other graphs in Ico. 10 | * 11 | * Options: 12 | * 13 | * `width`: The width of the container element 14 | * `height`: The height of the container element 15 | * `labels`: The textual labels 16 | * `label_count`: The number of numerical labels to display 17 | * `label_step`: The value to increment each numerical label 18 | * `start_value`: The value to start plotting from (generally 0) 19 | * `bar_size`: Set the size for a bar in a bar chart 20 | * `max_bar_size`: Set the maximum size for a bar in a bar chart 21 | * 22 | * @param {Object} A DOM element 23 | * @param {Array|Object} Data to display 24 | * @param {Object} Options 25 | * 26 | */ 27 | initialize: function(element, data, options) { 28 | options = options || {}; 29 | 30 | this.element = element.length ? element[0] : element; 31 | this.data_sets = this.buildDataSets(data, options); 32 | this.flat_data = this.flatten(data); 33 | this.data_size = this.longestDataSetLength(); 34 | this.plottedCoords = []; 35 | 36 | /* If one colour is specified, map it to a compatible set */ 37 | if (options && options.colour) { 38 | if (!options.colours) options.colours = {}; 39 | for (var key in this.data_sets) { 40 | if (this.data_sets.hasOwnProperty(key)) 41 | options.colours[key] = options.colour; 42 | } 43 | } 44 | 45 | this.options = { 46 | width: parseInt(getStyle(element, 'width'), 10), 47 | height: parseInt(getStyle(element, 'height'), 10), 48 | plot_padding: 10, // Padding for the graph line/bar plots 49 | font_size: 10, // Label font size 50 | show_horizontal_labels: true, 51 | show_vertical_labels: true, 52 | background_colour: getStyle(element, 'backgroundColor') || '#ffffff', 53 | label_colour: '#666', // Label text colour 54 | markers: false, // false, circle 55 | marker_size: 5, 56 | meanline: false, 57 | grid: false, 58 | grid_colour: '#ccc', 59 | y_padding_top: 20, 60 | draw: true, 61 | bar_labels: false // Display values on the top of bars 62 | }; 63 | 64 | Helpers.extend(this.options, this.chartDefaults() || {}); 65 | Helpers.extend(this.options, options); 66 | 67 | this.font_options = { 'font': this.options.font_size + 'px "Arial"', stroke: 'none', fill: '#000' }; 68 | this.normaliser = new Ico.Normaliser(this.flat_data, this.normaliserOptions()); 69 | this.label_step = options.label_step || this.normaliser.step; 70 | 71 | this.range = this.normaliser.range; 72 | this.start_value = options.start_value || this.normaliser.start_value; 73 | 74 | /* Padding around the graph area to make room for labels */ 75 | this.x_padding_left = 10 + this.paddingLeftOffset(); 76 | this.x_padding_right = 20; 77 | this.x_padding = this.x_padding_left + this.x_padding_right; 78 | this.y_padding_top = this.options.y_padding_top; 79 | 80 | this.y_padding_bottom = 20 + this.paddingBottomOffset(); 81 | this.y_padding = this.y_padding_top + this.y_padding_bottom; 82 | 83 | this.graph_width = this.options.width - (this.x_padding); 84 | this.graph_height = this.options.height - (this.y_padding); 85 | 86 | this.step = this.calculateStep(); 87 | this.grid_step = this.step; 88 | 89 | if (this.options.labels) { 90 | if (!this.grouped) { 91 | this.grid_step = this.graph_width / this.options.labels.length; 92 | this.snap_to_grid = true; 93 | } 94 | } else { 95 | this.options.labels = this.makeRange(1, this.data_size + 1); 96 | } 97 | 98 | if (this.options.bar_labels) { 99 | // TODO: Improve this so extra padding is used instead 100 | this.range += this.normaliser.step; 101 | } 102 | 103 | /* Calculate how many labels are required */ 104 | if (options.label_count) { 105 | this.y_label_count = options.label_count; 106 | } else { 107 | if (this.range === 0) { 108 | this.y_label_count = 1; 109 | this.label_step = 1; 110 | } else { 111 | this.y_label_count = Math.ceil(this.range / this.label_step); 112 | if ((this.normaliser.min + (this.y_label_count * this.normaliser.step)) < this.normaliser.max) { 113 | this.y_label_count += 1; 114 | } 115 | } 116 | } 117 | 118 | this.value_labels = this.makeValueLabels(this.y_label_count); 119 | this.top_value = this.value_labels[this.value_labels.length - 1]; 120 | 121 | /* Grid control options */ 122 | this.grid_start_offset = -1; 123 | 124 | /* Drawing */ 125 | if (this.options.draw) { 126 | if (typeof this.options.colours === 'undefined') { 127 | this.options.colours = this.makeRandomColours(); 128 | } 129 | 130 | this.paper = Raphael(this.element, this.options.width, this.options.height); 131 | this.background = this.paper.rect(0, 0, this.options.width, this.options.height); 132 | this.background.attr({fill: this.options.background_colour, stroke: 'none' }); 133 | 134 | if (this.options.meanline === true) { 135 | this.options.meanline = { 'stroke-width': '2px', stroke: '#BBBBBB' }; 136 | } 137 | 138 | this.setChartSpecificOptions(); 139 | this.lastPoint = { x: 0, y: 0 }; 140 | this.draw(); 141 | } 142 | }, 143 | 144 | buildDataSets: function(data, options) { 145 | return (typeof data.length !== 'undefined') ? { 'one': data } : data; 146 | }, 147 | 148 | normaliserOptions: function() { 149 | return {}; 150 | }, 151 | 152 | chartDefaults: function() { 153 | /* Define in child class */ 154 | return {}; 155 | }, 156 | 157 | drawPlot: function(index, pathString, x, y, colour) { 158 | /* Define in child class */ 159 | }, 160 | 161 | calculateStep: function() { 162 | /* Define in child classes */ 163 | }, 164 | 165 | makeRandomColours: function() { 166 | var colours; 167 | if (this.grouped) { 168 | colours = []; 169 | // Colours are supplied as integers for groups, because there's no obvious way to associate the bar name 170 | for (var i = 0; i < this.group_size; i++) { 171 | colours.push(Raphael.hsb2rgb(Math.random(), 1, 0.75).hex); 172 | } 173 | } else { 174 | colours = {}; 175 | for (var key in this.data_sets) { 176 | if (!colours.hasOwnProperty(key)) 177 | colours[key] = Raphael.hsb2rgb(Math.random(), 1, 0.75).hex; 178 | } 179 | } 180 | return colours; 181 | }, 182 | 183 | longestDataSetLength: function() { 184 | if (this.grouped) { 185 | // Return the total number of grouped values rather than 186 | // the longest array of data 187 | return this.flat_data.length; 188 | } 189 | 190 | var length = 0; 191 | for (var key in this.data_sets) { 192 | if (this.data_sets.hasOwnProperty(key)) { 193 | length = this.data_sets[key].length > length ? this.data_sets[key].length : length; 194 | } 195 | } 196 | return length; 197 | }, 198 | 199 | roundValue: function(value, length) { 200 | var multiplier = Math.pow(10, length); 201 | value *= multiplier; 202 | value = Math.round(value) / multiplier; 203 | return value; 204 | }, 205 | 206 | roundValues: function(data, length) { 207 | var roundedData = [], i; 208 | for (i = 0; i < data.length; i++) { 209 | roundedData.push(this.roundValue(data[i], length)); 210 | } 211 | return roundedData; 212 | }, 213 | 214 | longestLabel: function(values) { 215 | var labels = Array.prototype.slice.call(values || this.options.labels, 0); 216 | if (labels.length) { 217 | return labels.sort(function(a, b) { return a.toString().length < b.toString().length; })[0].toString().length; 218 | } 219 | return 0; 220 | }, 221 | 222 | paddingLeftOffset: function() { 223 | /* Find the longest label and multiply it by the font size */ 224 | var data = this.roundValues(this.flat_data, 2), 225 | longest_label_length = 0; 226 | 227 | longest_label_length = data.sort(function(a, b) { 228 | return a.toString().length < b.toString().length; 229 | })[0].toString().length; 230 | 231 | longest_label_length = longest_label_length > 2 ? longest_label_length - 1 : longest_label_length; 232 | return 10 + (longest_label_length * this.options.font_size); 233 | }, 234 | 235 | paddingBottomOffset: function() { 236 | /* Find the longest label and multiply it by the font size */ 237 | return this.options.font_size; 238 | }, 239 | 240 | normalise: function(value) { 241 | if (value === 0) { 242 | return 0; 243 | } 244 | 245 | var total = this.start_value === 0 ? this.top_value : this.range; 246 | return ((value / total) * (this.graph_height)); 247 | }, 248 | 249 | draw: function() { 250 | if (this.options.grid) { 251 | this.drawGrid(); 252 | } 253 | 254 | if (this.options.meanline) { 255 | this.drawMeanLine(this.normaliseData(this.flat_data)); 256 | } 257 | 258 | this.drawAxis(); 259 | 260 | if (this.options.show_vertical_labels) { 261 | this.drawVerticalLabels(); 262 | } 263 | 264 | if (this.options.show_horizontal_labels) { 265 | this.drawHorizontalLabels(); 266 | } 267 | 268 | if (this.grouped) { 269 | this.drawLines(key, null, this.normaliseData(this.flat_data)); 270 | } else { 271 | for (var key in this.data_sets) { 272 | if (this.data_sets.hasOwnProperty(key)) { 273 | var data = this.data_sets[key]; 274 | this.drawLines(key, this.options.colours[key], this.normaliseData(data)); 275 | } 276 | } 277 | } 278 | 279 | if (this.start_value !== 0) { 280 | this.drawFocusHint(); 281 | } 282 | }, 283 | 284 | drawGrid: function() { 285 | var pathString = '', i, y; 286 | 287 | if (this.options.show_vertical_labels) { 288 | y = this.graph_height + this.y_padding_top; 289 | for (i = 0; i < this.y_label_count; i++) { 290 | y = y - (this.graph_height / this.y_label_count); 291 | pathString += 'M' + this.x_padding_left + ',' + y; 292 | pathString += 'L' + (this.x_padding_left + this.graph_width) + ',' + y; 293 | } 294 | } 295 | 296 | if (this.options.show_horizontal_labels) { 297 | var x = this.x_padding_left + this.options.plot_padding + this.grid_start_offset, 298 | x_labels = this.grouped ? this.flat_data.length : this.options.labels.length, 299 | i, 300 | step = this.grid_step || this.step; 301 | 302 | for (i = 0; i < x_labels; i++) { 303 | pathString += 'M' + x + ',' + this.y_padding_top; 304 | pathString += 'L' + x +',' + (this.y_padding_top + this.graph_height); 305 | x = x + step; 306 | } 307 | 308 | x = x - this.options.plot_padding - 1; 309 | pathString += 'M' + x + ',' + this.y_padding_top; 310 | pathString += 'L' + x + ',' + (this.y_padding_top + this.graph_height); 311 | } 312 | 313 | this.paper.path(pathString).attr({ stroke: this.options.grid_colour, 'stroke-width': '1px' }); 314 | }, 315 | 316 | drawLines: function(label, colour, data) { 317 | var coords = this.calculateCoords(data), 318 | pathString = '', 319 | i, x, y; 320 | 321 | for (i = 0; i < coords.length; i++) { 322 | x = coords[i][0] || 0; 323 | y = coords[i][1] || 0; 324 | 325 | if (this.grouped && this.options.colours) { 326 | colour = this.options.colours[i % this.group_size]; 327 | } 328 | 329 | this.plottedCoords.push([x + this.bar_padding, y]); 330 | pathString = this.drawPlot(i, pathString, x, y, colour); 331 | } 332 | 333 | this.paper.path(pathString).attr({ stroke: colour, 'stroke-width': this.options.stroke_width }); 334 | 335 | if (this.options.bar_labels) { 336 | this.drawBarMarkers(); 337 | } 338 | 339 | if (this.options.line) { 340 | this.additionalLine(label, colour, this.normaliseData(this.options.line)); 341 | } 342 | }, 343 | 344 | additionalLine: function(label, colour, data) { 345 | var coords = this.calculateCoords(data), 346 | pathString = '', 347 | i, x, y, step, lineWidth = 3; 348 | 349 | if (this.grouped) { 350 | step = this.step * (this.group_size - 1); 351 | } 352 | 353 | for (i = 0; i < coords.length; i++) { 354 | x = coords[i][0] || 0; 355 | y = coords[i][1] || 0; 356 | 357 | if (this.grouped) { 358 | x += (step * i) + this.roundValue(step / 2, 0); 359 | } 360 | x += lineWidth; 361 | 362 | pathString = Ico.LineGraph.prototype.drawPlot.apply(this, [i, pathString, this.roundValue(x, 0), y, colour]); 363 | } 364 | 365 | this.paper.path(pathString).attr({ stroke: '#ff0000', 'stroke-width': lineWidth + 'px' }); 366 | }, 367 | 368 | calculateCoords: function(data) { 369 | var x = this.x_padding_left + this.options.plot_padding - this.step, 370 | y_offset = (this.graph_height + this.y_padding_top) + this.normalise(this.start_value), 371 | y = 0, 372 | coords = [], 373 | i; 374 | 375 | for (i = 0; i < data.length; i++) { 376 | y = y_offset - data[i]; 377 | x = x + this.step; 378 | coords.push([x, y]); 379 | } 380 | 381 | return coords; 382 | }, 383 | 384 | drawFocusHint: function() { 385 | var length = 5, 386 | x = this.x_padding_left + (length / 2) - 1, 387 | y = this.options.height - this.y_padding_bottom, 388 | pathString = ''; 389 | 390 | pathString += 'M' + x + ',' + y; 391 | pathString += 'L' + (x - length) + ',' + (y - length); 392 | pathString += 'M' + x + ',' + (y - length); 393 | pathString += 'L' + (x - length) + ',' + (y - (length * 2)); 394 | this.paper.path(pathString).attr({ stroke: this.options.label_colour, 'stroke-width': 2 }); 395 | }, 396 | 397 | drawMeanLine: function(data) { 398 | var offset = Helpers.sum(data) / data.length, 399 | pathString = ''; 400 | 401 | pathString += 'M' + (this.x_padding_left - 1) + ',' + (this.options.height - this.y_padding_bottom - offset); 402 | pathString += 'L' + (this.graph_width + this.x_padding_left) + ',' + (this.options.height - this.y_padding_bottom - offset); 403 | this.paper.path(pathString).attr(this.options.meanline); 404 | }, 405 | 406 | drawAxis: function() { 407 | var pathString = ''; 408 | 409 | pathString += 'M' + (this.x_padding_left - 1) + ',' + (this.options.height - this.y_padding_bottom); 410 | pathString += 'L' + (this.graph_width + this.x_padding_left) + ',' + (this.options.height - this.y_padding_bottom); 411 | pathString += 'M' + (this.x_padding_left - 1) + ',' + (this.options.height - this.y_padding_bottom); 412 | pathString += 'L' + (this.x_padding_left - 1) + ',' + (this.y_padding_top); 413 | 414 | this.paper.path(pathString).attr({ stroke: this.options.label_colour }); 415 | }, 416 | 417 | makeValueLabels: function(steps) { 418 | var step = this.label_step, 419 | label = this.start_value, 420 | labels = []; 421 | 422 | for (var i = 0; i < steps; i++) { 423 | label = this.roundValue((label + step), 2); 424 | labels.push(label); 425 | } 426 | return labels; 427 | }, 428 | 429 | /* Axis label markers */ 430 | drawMarkers: function(labels, direction, step, start_offset, font_offsets, extra_font_options) { 431 | function x_offset(value) { 432 | return value * direction[0]; 433 | } 434 | 435 | function y_offset(value) { 436 | return value * direction[1]; 437 | } 438 | 439 | /* Start at the origin */ 440 | var x = this.x_padding_left - 1 + x_offset(start_offset), 441 | y = this.options.height - this.y_padding_bottom + y_offset(start_offset), 442 | pathString = '', 443 | i, 444 | font_options = {}; 445 | Helpers.extend(font_options, this.font_options); 446 | Helpers.extend(font_options, extra_font_options || {}); 447 | 448 | for (i = 0; i < labels.length; i++) { 449 | pathString += 'M' + x + ',' + y; 450 | if (typeof labels[i] !== 'undefined' && (labels[i] + '').length > 0) { 451 | pathString += 'L' + (x + y_offset(5)) + ',' + (y + x_offset(5)); 452 | this.paper.text(x + font_offsets[0], y - font_offsets[1], labels[i]).attr(font_options).toFront(); 453 | } 454 | x = x + x_offset(step); 455 | y = y + y_offset(step); 456 | } 457 | 458 | this.paper.path(pathString).attr({ stroke: this.options.label_colour }); 459 | }, 460 | 461 | drawVerticalLabels: function() { 462 | var y_step = this.graph_height / this.y_label_count; 463 | this.drawMarkers(this.value_labels, [0, -1], y_step, y_step, [-8, -2], { 'text-anchor': 'end' }); 464 | }, 465 | 466 | drawHorizontalLabels: function() { 467 | var step = this.snap_to_grid ? this.grid_step : this.step; 468 | this.drawMarkers(this.options.labels, [1, 0], step, this.options.plot_padding, [0, (this.options.font_size + 7) * -1]); 469 | } 470 | }); 471 | -------------------------------------------------------------------------------- /ico.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Ico 3 | * Copyright (C) 2009-2011 Alex R. Young 4 | * MIT Licensed 5 | *//** 6 | * The Ico object. 7 | */(function(global){function validStepDivider(value){return value>1?value-1:1}function getStyle(el,styleProp){if(typeof window=="undefined")return;var style;return el.currentStyle?style=el.currentStyle[styleProp]:window.getComputedStyle&&(style=document.defaultView.getComputedStyle(el,null).getPropertyValue(styleProp)),style&&style.length===0&&(style=null),style}var Ico={VERSION:"0.3.10",round:function(num,dec){var result=Math.round(num*Math.pow(10,dec))/Math.pow(10,dec);return result}},Helpers={};Helpers.sum=function(a){var i,sum;for(i=0,sum=0;i=0?this.options.start_value:this.min,start_value=this.round(min,1);return this.min>0&&start_value>this.min?0:this.min===this.max?0:start_value},round:function(value,offset){offset=offset||1;var roundedValue=value;if(this.standard_deviation>.1){var multiplier=Math.pow(10,-offset);roundedValue=Math.round(value*multiplier)/multiplier;if(roundedValue>this.min)return this.round(value-this.step)}return roundedValue},process:function(){this.range=this.max-this.start_value,this.step=this.labelStep(this.range)},labelStep:function(value){return Math.pow(10,Math.round(Math.log(value)/Math.LN10)-1)}},Ico.Base={normaliseData:function(data){var values=[],i=0;for(i=0;ilength?this.data_sets[key].length:length);return length},roundValue:function(value,length){var multiplier=Math.pow(10,length);return value*=multiplier,value=Math.round(value)/multiplier,value},roundValues:function(data,length){var roundedData=[],i;for(i=0;i2?longest_label_length-1:longest_label_length,10+longest_label_length*this.options.font_size},paddingBottomOffset:function(){return this.options.font_size},normalise:function(value){if(value===0)return 0;var total=this.start_value===0?this.top_value:this.range;return value/total*this.graph_height},draw:function(){this.options.grid&&this.drawGrid(),this.options.meanline&&this.drawMeanLine(this.normaliseData(this.flat_data)),this.drawAxis(),this.options.show_vertical_labels&&this.drawVerticalLabels(),this.options.show_horizontal_labels&&this.drawHorizontalLabels();if(this.grouped)this.drawLines(key,null,this.normaliseData(this.flat_data));else for(var key in this.data_sets)if(this.data_sets.hasOwnProperty(key)){var data=this.data_sets[key];this.drawLines(key,this.options.colours[key],this.normaliseData(data))}this.start_value!==0&&this.drawFocusHint()},drawGrid:function(){var pathString="",i,y;if(this.options.show_vertical_labels){y=this.graph_height+this.y_padding_top;for(i=0;i0&&(pathString+="L"+(x+y_offset(5))+","+(y+x_offset(5)),this.paper.text(x+font_offsets[0],y-font_offsets[1],labels[i]).attr(font_options).toFront()),x+=x_offset(step),y+=y_offset(step);this.paper.path(pathString).attr({stroke:this.options.label_colour})},drawVerticalLabels:function(){var y_step=this.graph_height/this.y_label_count;this.drawMarkers(this.value_labels,[0,-1],y_step,y_step,[-8,-2],{"text-anchor":"end"})},drawHorizontalLabels:function(){var step=this.snap_to_grid?this.grid_step:this.step;this.drawMarkers(this.options.labels,[1,0],step,this.options.plot_padding,[0,(this.options.font_size+7)*-1])}}),Ico.BarGraph=function(){this.initialize.apply(this,arguments)},Helpers.extend(Ico.BarGraph.prototype,Ico.BaseGraph.prototype),Helpers.extend(Ico.BarGraph.prototype,{buildDataSets:function(data,options){if(typeof data.length!="undefined"){if(typeof data[0].length!="undefined"){this.grouped=!0,this.group_size=data[0].length;var o={},k,i=0;for(k in options.labels)k=options.labels[k],o[k]=data[i],i++;return o}return{one:data}}return data},chartDefaults:function(){return{plot_padding:0}},normaliserOptions:function(){return this.options.line?{start_value:0,max:Helpers.max([Helpers.max(this.options.line),Helpers.max(this.flat_data)])}:{start_value:0}},setChartSpecificOptions:function(){this.bar_padding=this.options.bar_padding||5,this.bar_width=this.options.bar_size||this.calculateBarWidth(),this.options.bar_size&&!this.options.bar_padding&&(this.bar_padding=this.graph_width/this.data_size),this.options.plot_padding=this.bar_width/2-this.bar_padding/2,this.step=this.calculateStep(),this.grid_start_offset=this.bar_padding-1,this.start_y=this.options.height-this.y_padding_bottom},calculateBarWidth:function(){var width=this.graph_width/this.data_size-this.bar_padding;return!this.grouped,this.options.max_bar_size&&width>this.options.max_bar_size&&(width=this.options.max_bar_size,this.bar_padding=this.graph_width/this.data_size),width},calculateStep:function(){return(this.graph_width-this.options.plot_padding*2-this.bar_padding*2)/validStepDivider(this.data_size)},drawPlot:function(index,pathString,x,y,colour){return this.options.highlight_colours&&this.options.highlight_colours.hasOwnProperty(index)&&(colour=this.options.highlight_colours[index]),x+=this.bar_padding,pathString+="M"+x+","+this.start_y,pathString+="L"+x+","+y,this.paper.path(pathString).attr({stroke:colour,"stroke-width":this.bar_width+"px"}),pathString="",x+=this.step,pathString+="M"+x+","+this.start_y,pathString},drawHorizontalLabels:function(){var x_start=this.bar_padding+this.options.plot_padding,step=this.step;this.grouped&&(step*=this.group_size,x_start=(this.bar_width*this.group_size+this.bar_padding*this.group_size)/2,x_start=this.roundValue(x_start,0)),this.drawMarkers(this.options.labels,[1,0],step,x_start,[0,(this.options.font_size+7)*-1])},drawBarMarkers:function(){if(this.plottedCoords.length===0)return;var i,length=this.flat_data.length,x,y,label,font_options={};Helpers.extend(font_options,this.font_options),font_options["text-anchor"]="center";for(i=0;ithis.options.max_bar_size&&(height=this.options.max_bar_size,this.bar_padding=this.graph_height/this.data_size),height},calculateStep:function(){return(this.options.height-this.y_padding_bottom)/validStepDivider(this.data_size)},drawLines:function(label,colour,data){var x=this.x_padding_left+this.options.plot_padding*2,y=this.options.height-this.y_padding_bottom-this.step/2,pathString="M"+x+","+y,i;for(i=0;i2?this.step-1:this.step,x=width,pathString="",i=0;for(i=0;i1?value-1:1}function getStyle(el,styleProp){if(typeof window=="undefined")return;var style;return el.currentStyle?style=el.currentStyle[styleProp]:window.getComputedStyle&&(style=document.defaultView.getComputedStyle(el,null).getPropertyValue(styleProp)),style&&style.length===0&&(style=null),style}var Ico={VERSION:"0.3.10",round:function(num,dec){var result=Math.round(num*Math.pow(10,dec))/Math.pow(10,dec);return result}},Helpers={};Helpers.sum=function(a){var i,sum;for(i=0,sum=0;i=0?this.options.start_value:this.min,start_value=this.round(min,1);return this.min>0&&start_value>this.min?0:this.min===this.max?0:start_value},round:function(value,offset){offset=offset||1;var roundedValue=value;if(this.standard_deviation>.1){var multiplier=Math.pow(10,-offset);roundedValue=Math.round(value*multiplier)/multiplier;if(roundedValue>this.min)return this.round(value-this.step)}return roundedValue},process:function(){this.range=this.max-this.start_value,this.step=this.labelStep(this.range)},labelStep:function(value){return Math.pow(10,Math.round(Math.log(value)/Math.LN10)-1)}},Ico.Base={normaliseData:function(data){var values=[],i=0;for(i=0;ilength?this.data_sets[key].length:length);return length},roundValue:function(value,length){var multiplier=Math.pow(10,length);return value*=multiplier,value=Math.round(value)/multiplier,value},roundValues:function(data,length){var roundedData=[],i;for(i=0;i2?longest_label_length-1:longest_label_length,10+longest_label_length*this.options.font_size},paddingBottomOffset:function(){return this.options.font_size},normalise:function(value){if(value===0)return 0;var total=this.start_value===0?this.top_value:this.range;return value/total*this.graph_height},draw:function(){this.options.grid&&this.drawGrid(),this.options.meanline&&this.drawMeanLine(this.normaliseData(this.flat_data)),this.drawAxis(),this.options.show_vertical_labels&&this.drawVerticalLabels(),this.options.show_horizontal_labels&&this.drawHorizontalLabels();if(this.grouped)this.drawLines(key,null,this.normaliseData(this.flat_data));else for(var key in this.data_sets)if(this.data_sets.hasOwnProperty(key)){var data=this.data_sets[key];this.drawLines(key,this.options.colours[key],this.normaliseData(data))}this.start_value!==0&&this.drawFocusHint()},drawGrid:function(){var pathString="",i,y;if(this.options.show_vertical_labels){y=this.graph_height+this.y_padding_top;for(i=0;i0&&(pathString+="L"+(x+y_offset(5))+","+(y+x_offset(5)),this.paper.text(x+font_offsets[0],y-font_offsets[1],labels[i]).attr(font_options).toFront()),x+=x_offset(step),y+=y_offset(step);this.paper.path(pathString).attr({stroke:this.options.label_colour})},drawVerticalLabels:function(){var y_step=this.graph_height/this.y_label_count;this.drawMarkers(this.value_labels,[0,-1],y_step,y_step,[-8,-2],{"text-anchor":"end"})},drawHorizontalLabels:function(){var step=this.snap_to_grid?this.grid_step:this.step;this.drawMarkers(this.options.labels,[1,0],step,this.options.plot_padding,[0,(this.options.font_size+7)*-1])}}),Ico.BarGraph=function(){this.initialize.apply(this,arguments)},Helpers.extend(Ico.BarGraph.prototype,Ico.BaseGraph.prototype),Helpers.extend(Ico.BarGraph.prototype,{buildDataSets:function(data,options){if(typeof data.length!="undefined"){if(typeof data[0].length!="undefined"){this.grouped=!0,this.group_size=data[0].length;var o={},k,i=0;for(k in options.labels)k=options.labels[k],o[k]=data[i],i++;return o}return{one:data}}return data},chartDefaults:function(){return{plot_padding:0}},normaliserOptions:function(){return this.options.line?{start_value:0,max:Helpers.max([Helpers.max(this.options.line),Helpers.max(this.flat_data)])}:{start_value:0}},setChartSpecificOptions:function(){this.bar_padding=this.options.bar_padding||5,this.bar_width=this.options.bar_size||this.calculateBarWidth(),this.options.bar_size&&!this.options.bar_padding&&(this.bar_padding=this.graph_width/this.data_size),this.options.plot_padding=this.bar_width/2-this.bar_padding/2,this.step=this.calculateStep(),this.grid_start_offset=this.bar_padding-1,this.start_y=this.options.height-this.y_padding_bottom},calculateBarWidth:function(){var width=this.graph_width/this.data_size-this.bar_padding;return!this.grouped,this.options.max_bar_size&&width>this.options.max_bar_size&&(width=this.options.max_bar_size,this.bar_padding=this.graph_width/this.data_size),width},calculateStep:function(){return(this.graph_width-this.options.plot_padding*2-this.bar_padding*2)/validStepDivider(this.data_size)},drawPlot:function(index,pathString,x,y,colour){return this.options.highlight_colours&&this.options.highlight_colours.hasOwnProperty(index)&&(colour=this.options.highlight_colours[index]),x+=this.bar_padding,pathString+="M"+x+","+this.start_y,pathString+="L"+x+","+y,this.paper.path(pathString).attr({stroke:colour,"stroke-width":this.bar_width+"px"}),pathString="",x+=this.step,pathString+="M"+x+","+this.start_y,pathString},drawHorizontalLabels:function(){var x_start=this.bar_padding+this.options.plot_padding,step=this.step;this.grouped&&(step*=this.group_size,x_start=(this.bar_width*this.group_size+this.bar_padding*this.group_size)/2,x_start=this.roundValue(x_start,0)),this.drawMarkers(this.options.labels,[1,0],step,x_start,[0,(this.options.font_size+7)*-1])},drawBarMarkers:function(){if(this.plottedCoords.length===0)return;var i,length=this.flat_data.length,x,y,label,font_options={};Helpers.extend(font_options,this.font_options),font_options["text-anchor"]="center";for(i=0;ithis.options.max_bar_size&&(height=this.options.max_bar_size,this.bar_padding=this.graph_height/this.data_size),height},calculateStep:function(){return(this.options.height-this.y_padding_bottom)/validStepDivider(this.data_size)},drawLines:function(label,colour,data){var x=this.x_padding_left+this.options.plot_padding*2,y=this.options.height-this.y_padding_bottom-this.step/2,pathString="M"+x+","+y,i;for(i=0;i2?this.step-1:this.step,x=width,pathString="",i=0;for(i=0;i 1 ? value - 1 : 1; 36 | } 37 | 38 | /** 39 | * Gets a CSS style property. 40 | * 41 | * @param {Object} el A DOM element 42 | * @param {String} styleProp The name of a style property 43 | * @returns {Object} The style value 44 | */ 45 | function getStyle(el, styleProp) { 46 | if (typeof window === 'undefined') { 47 | return; 48 | } 49 | 50 | var style; 51 | if (el.currentStyle) { 52 | style = el.currentStyle[styleProp]; 53 | } else if (window.getComputedStyle) { 54 | style = document.defaultView.getComputedStyle(el, null).getPropertyValue(styleProp); 55 | } 56 | if (style && style.length === 0) { 57 | style = null; 58 | } 59 | return style; 60 | } 61 | 62 | var Helpers = {}; 63 | 64 | Helpers.sum = function(a) { 65 | var i, sum; 66 | for (i = 0, sum = 0; i < a.length; sum += a[i++]) {} 67 | return sum; 68 | }; 69 | 70 | if (typeof Array.prototype.max === 'undefined') { 71 | Helpers.max = function(a) { 72 | return Math.max.apply({}, a); 73 | }; 74 | } else { 75 | Helpers.max = function(a) { 76 | return a.max(); 77 | }; 78 | } 79 | 80 | if (typeof Array.prototype.min === 'undefined') { 81 | Helpers.min = function(a) { 82 | return Math.min.apply({}, a); 83 | }; 84 | } else { 85 | Helpers.min = function(a) { 86 | return a.min(); 87 | }; 88 | } 89 | 90 | Helpers.mean = function(a) { 91 | return Helpers.sum(a) / a.length; 92 | }; 93 | 94 | Helpers.variance = function(a) { 95 | var mean = Helpers.mean(a), 96 | variance = 0; 97 | for (var i = 0; i < a.length; i++) { 98 | variance += Math.pow(a[i] - mean, 2); 99 | } 100 | return variance / (a.length - 1); 101 | }; 102 | 103 | Helpers.standard_deviation = function(a) { 104 | return Math.sqrt(Helpers.variance(a)); 105 | }; 106 | 107 | if (typeof Object.extend === 'undefined') { 108 | Helpers.extend = function(destination, source) { 109 | for (var property in source) { 110 | if (source.hasOwnProperty(property)) { 111 | destination[property] = source[property]; 112 | } 113 | } 114 | return destination; 115 | }; 116 | } else { 117 | Helpers.extend = Object.extend; 118 | } 119 | 120 | if (Object.keys) { 121 | Helpers.keys = Object.keys; 122 | } else { 123 | Helpers.keys = function(o) { 124 | if (o !== Object(o)) { 125 | throw new TypeError('Object.keys called on non-object'); 126 | } 127 | 128 | var ret = [], p; 129 | for (p in o) { 130 | if (Object.prototype.hasOwnProperty.call(o,p)) { 131 | ret.push(p); 132 | } 133 | } 134 | return ret; 135 | } 136 | } 137 | /** 138 | * Normalises lists of values to fit inside a graph. 139 | * 140 | * @param {Array} data A list of values 141 | * @param {Object} options Can be used to set the `start_value` 142 | */ 143 | Ico.Normaliser = function(data, options) { 144 | this.options = { 145 | start_value: null 146 | }; 147 | 148 | if (typeof options !== 'undefined') { 149 | this.options = options; 150 | } 151 | 152 | this.min = Helpers.min(data); 153 | this.max = this.options.max || Helpers.max(data); 154 | this.standard_deviation = Helpers.standard_deviation(data); 155 | this.range = 0; 156 | this.step = this.labelStep(this.max - this.min); 157 | this.start_value = this.calculateStart(); 158 | this.process(); 159 | }; 160 | 161 | Ico.Normaliser.prototype = { 162 | /** 163 | * Calculates the start value. This is often 0. 164 | * @returns {Float} The start value 165 | */ 166 | calculateStart: function() { 167 | var min = typeof this.options.start_value !== 'undefined' && this.min >= 0 ? this.options.start_value : this.min, 168 | start_value = this.round(min, 1); 169 | 170 | /* This is a boundary condition */ 171 | if (this.min > 0 && start_value > this.min) { 172 | return 0; 173 | } 174 | 175 | if (this.min === this.max) { 176 | return 0; 177 | } 178 | 179 | return start_value; 180 | }, 181 | 182 | /* Given a value, this method rounds it to the nearest good value for an origin */ 183 | round: function(value, offset) { 184 | offset = offset || 1; 185 | var roundedValue = value; 186 | 187 | if (this.standard_deviation > 0.1) { 188 | var multiplier = Math.pow(10, -offset); 189 | roundedValue = Math.round(value * multiplier) / multiplier; 190 | 191 | if (roundedValue > this.min) { 192 | return this.round(value - this.step); 193 | } 194 | } 195 | return roundedValue; 196 | }, 197 | 198 | /** 199 | * Calculates the range and step values. 200 | */ 201 | process: function() { 202 | this.range = this.max - this.start_value; 203 | this.step = this.labelStep(this.range); 204 | }, 205 | 206 | /** 207 | * Calculates the label step value. 208 | * 209 | * @param {Float} value A value to convert to a label position 210 | * @returns {Float} The rounded label step result 211 | */ 212 | labelStep: function(value) { 213 | return Math.pow(10, Math.round((Math.log(value) / Math.LN10)) - 1); 214 | } 215 | }; 216 | 217 | 218 | /*! 219 | * Ico 220 | * Copyright (C) 2009-2011 Alex R. Young 221 | * MIT Licensed 222 | */ 223 | 224 | /** 225 | * The Ico.Base object which contains useful generic functions. 226 | */ 227 | Ico.Base = { 228 | /** 229 | * Runs this.normalise on each value. 230 | * 231 | * @param {Array} data Values to normalise 232 | * @returns {Array} Normalised values 233 | */ 234 | normaliseData: function(data) { 235 | var values = [], 236 | i = 0; 237 | for (i = 0; i < data.length; i++) { 238 | values.push(this.normalise(data[i])); 239 | } 240 | return values; 241 | }, 242 | 243 | /** 244 | * Flattens objects into an array. 245 | * 246 | * @param {Object} data Values to flatten 247 | * @returns {Array} Flattened values 248 | */ 249 | flatten: function(data) { 250 | var flat_data = []; 251 | 252 | if (typeof data.length === 'undefined') { 253 | if (typeof data === 'object') { 254 | for (var key in data) { 255 | if (data.hasOwnProperty(key)) 256 | flat_data = flat_data.concat(this.flatten(data[key])); 257 | } 258 | } else { 259 | return []; 260 | } 261 | } 262 | 263 | for (var i = 0; i < data.length; i++) { 264 | if (typeof data[i].length === 'number') { 265 | flat_data = flat_data.concat(this.flatten(data[i])); 266 | } else { 267 | flat_data.push(data[i]); 268 | } 269 | } 270 | return flat_data; 271 | }, 272 | 273 | /** 274 | * Handy method to produce an array of numbers. 275 | * 276 | * @param {Integer} start A number to start at 277 | * @param {Integer} end A number to end at 278 | * @returns {Array} An array of values 279 | */ 280 | makeRange: function(start, end, options) { 281 | var values = [], i; 282 | for (i = start; i < end; i++) { 283 | if (options && options.skip) { 284 | if (i % options.skip === 0) { 285 | values.push(i); 286 | } else { 287 | values.push(undefined); 288 | } 289 | } else { 290 | values.push(i); 291 | } 292 | } 293 | return values; 294 | } 295 | }; 296 | /** 297 | * Ico.BaseGraph is extended by most of the other graphs. It 298 | * uses a simple pattern with methods that can be overridden. 299 | */ 300 | Ico.BaseGraph = function() { this.initialize.apply(this, arguments); }; 301 | Helpers.extend(Ico.BaseGraph.prototype, Ico.Base); 302 | Helpers.extend(Ico.BaseGraph.prototype, { 303 | /** 304 | * This base class is used by the other graphs in Ico. 305 | * 306 | * Options: 307 | * 308 | * `width`: The width of the container element 309 | * `height`: The height of the container element 310 | * `labels`: The textual labels 311 | * `label_count`: The number of numerical labels to display 312 | * `label_step`: The value to increment each numerical label 313 | * `start_value`: The value to start plotting from (generally 0) 314 | * `bar_size`: Set the size for a bar in a bar chart 315 | * `max_bar_size`: Set the maximum size for a bar in a bar chart 316 | * 317 | * @param {Object} A DOM element 318 | * @param {Array|Object} Data to display 319 | * @param {Object} Options 320 | * 321 | */ 322 | initialize: function(element, data, options) { 323 | options = options || {}; 324 | 325 | this.element = element.length ? element[0] : element; 326 | this.data_sets = this.buildDataSets(data, options); 327 | this.flat_data = this.flatten(data); 328 | this.data_size = this.longestDataSetLength(); 329 | this.plottedCoords = []; 330 | 331 | /* If one colour is specified, map it to a compatible set */ 332 | if (options && options.colour) { 333 | if (!options.colours) options.colours = {}; 334 | for (var key in this.data_sets) { 335 | if (this.data_sets.hasOwnProperty(key)) 336 | options.colours[key] = options.colour; 337 | } 338 | } 339 | 340 | this.options = { 341 | width: parseInt(getStyle(element, 'width'), 10), 342 | height: parseInt(getStyle(element, 'height'), 10), 343 | plot_padding: 10, // Padding for the graph line/bar plots 344 | font_size: 10, // Label font size 345 | show_horizontal_labels: true, 346 | show_vertical_labels: true, 347 | background_colour: getStyle(element, 'backgroundColor') || '#ffffff', 348 | label_colour: '#666', // Label text colour 349 | markers: false, // false, circle 350 | marker_size: 5, 351 | meanline: false, 352 | grid: false, 353 | grid_colour: '#ccc', 354 | y_padding_top: 20, 355 | draw: true, 356 | bar_labels: false // Display values on the top of bars 357 | }; 358 | 359 | Helpers.extend(this.options, this.chartDefaults() || {}); 360 | Helpers.extend(this.options, options); 361 | 362 | this.font_options = { 'font': this.options.font_size + 'px "Arial"', stroke: 'none', fill: '#000' }; 363 | this.normaliser = new Ico.Normaliser(this.flat_data, this.normaliserOptions()); 364 | this.label_step = options.label_step || this.normaliser.step; 365 | 366 | this.range = this.normaliser.range; 367 | this.start_value = options.start_value || this.normaliser.start_value; 368 | 369 | /* Padding around the graph area to make room for labels */ 370 | this.x_padding_left = 10 + this.paddingLeftOffset(); 371 | this.x_padding_right = 20; 372 | this.x_padding = this.x_padding_left + this.x_padding_right; 373 | this.y_padding_top = this.options.y_padding_top; 374 | 375 | this.y_padding_bottom = 20 + this.paddingBottomOffset(); 376 | this.y_padding = this.y_padding_top + this.y_padding_bottom; 377 | 378 | this.graph_width = this.options.width - (this.x_padding); 379 | this.graph_height = this.options.height - (this.y_padding); 380 | 381 | this.step = this.calculateStep(); 382 | this.grid_step = this.step; 383 | 384 | if (this.options.labels) { 385 | if (!this.grouped) { 386 | this.grid_step = this.graph_width / this.options.labels.length; 387 | this.snap_to_grid = true; 388 | } 389 | } else { 390 | this.options.labels = this.makeRange(1, this.data_size + 1); 391 | } 392 | 393 | if (this.options.bar_labels) { 394 | // TODO: Improve this so extra padding is used instead 395 | this.range += this.normaliser.step; 396 | } 397 | 398 | /* Calculate how many labels are required */ 399 | if (options.label_count) { 400 | this.y_label_count = options.label_count; 401 | } else { 402 | if (this.range === 0) { 403 | this.y_label_count = 1; 404 | this.label_step = 1; 405 | } else { 406 | this.y_label_count = Math.ceil(this.range / this.label_step); 407 | if ((this.normaliser.min + (this.y_label_count * this.normaliser.step)) < this.normaliser.max) { 408 | this.y_label_count += 1; 409 | } 410 | } 411 | } 412 | 413 | this.value_labels = this.makeValueLabels(this.y_label_count); 414 | this.top_value = this.value_labels[this.value_labels.length - 1]; 415 | 416 | /* Grid control options */ 417 | this.grid_start_offset = -1; 418 | 419 | /* Drawing */ 420 | if (this.options.draw) { 421 | if (typeof this.options.colours === 'undefined') { 422 | this.options.colours = this.makeRandomColours(); 423 | } 424 | 425 | this.paper = Raphael(this.element, this.options.width, this.options.height); 426 | this.background = this.paper.rect(0, 0, this.options.width, this.options.height); 427 | this.background.attr({fill: this.options.background_colour, stroke: 'none' }); 428 | 429 | if (this.options.meanline === true) { 430 | this.options.meanline = { 'stroke-width': '2px', stroke: '#BBBBBB' }; 431 | } 432 | 433 | this.setChartSpecificOptions(); 434 | this.lastPoint = { x: 0, y: 0 }; 435 | this.draw(); 436 | } 437 | }, 438 | 439 | buildDataSets: function(data, options) { 440 | return (typeof data.length !== 'undefined') ? { 'one': data } : data; 441 | }, 442 | 443 | normaliserOptions: function() { 444 | return {}; 445 | }, 446 | 447 | chartDefaults: function() { 448 | /* Define in child class */ 449 | return {}; 450 | }, 451 | 452 | drawPlot: function(index, pathString, x, y, colour) { 453 | /* Define in child class */ 454 | }, 455 | 456 | calculateStep: function() { 457 | /* Define in child classes */ 458 | }, 459 | 460 | makeRandomColours: function() { 461 | var colours; 462 | if (this.grouped) { 463 | colours = []; 464 | // Colours are supplied as integers for groups, because there's no obvious way to associate the bar name 465 | for (var i = 0; i < this.group_size; i++) { 466 | colours.push(Raphael.hsb2rgb(Math.random(), 1, 0.75).hex); 467 | } 468 | } else { 469 | colours = {}; 470 | for (var key in this.data_sets) { 471 | if (!colours.hasOwnProperty(key)) 472 | colours[key] = Raphael.hsb2rgb(Math.random(), 1, 0.75).hex; 473 | } 474 | } 475 | return colours; 476 | }, 477 | 478 | longestDataSetLength: function() { 479 | if (this.grouped) { 480 | // Return the total number of grouped values rather than 481 | // the longest array of data 482 | return this.flat_data.length; 483 | } 484 | 485 | var length = 0; 486 | for (var key in this.data_sets) { 487 | if (this.data_sets.hasOwnProperty(key)) { 488 | length = this.data_sets[key].length > length ? this.data_sets[key].length : length; 489 | } 490 | } 491 | return length; 492 | }, 493 | 494 | roundValue: function(value, length) { 495 | var multiplier = Math.pow(10, length); 496 | value *= multiplier; 497 | value = Math.round(value) / multiplier; 498 | return value; 499 | }, 500 | 501 | roundValues: function(data, length) { 502 | var roundedData = [], i; 503 | for (i = 0; i < data.length; i++) { 504 | roundedData.push(this.roundValue(data[i], length)); 505 | } 506 | return roundedData; 507 | }, 508 | 509 | longestLabel: function(values) { 510 | var labels = Array.prototype.slice.call(values || this.options.labels, 0); 511 | if (labels.length) { 512 | return labels.sort(function(a, b) { return a.toString().length < b.toString().length; })[0].toString().length; 513 | } 514 | return 0; 515 | }, 516 | 517 | paddingLeftOffset: function() { 518 | /* Find the longest label and multiply it by the font size */ 519 | var data = this.roundValues(this.flat_data, 2), 520 | longest_label_length = 0; 521 | 522 | longest_label_length = data.sort(function(a, b) { 523 | return a.toString().length < b.toString().length; 524 | })[0].toString().length; 525 | 526 | longest_label_length = longest_label_length > 2 ? longest_label_length - 1 : longest_label_length; 527 | return 10 + (longest_label_length * this.options.font_size); 528 | }, 529 | 530 | paddingBottomOffset: function() { 531 | /* Find the longest label and multiply it by the font size */ 532 | return this.options.font_size; 533 | }, 534 | 535 | normalise: function(value) { 536 | if (value === 0) { 537 | return 0; 538 | } 539 | 540 | var total = this.start_value === 0 ? this.top_value : this.range; 541 | return ((value / total) * (this.graph_height)); 542 | }, 543 | 544 | draw: function() { 545 | if (this.options.grid) { 546 | this.drawGrid(); 547 | } 548 | 549 | if (this.options.meanline) { 550 | this.drawMeanLine(this.normaliseData(this.flat_data)); 551 | } 552 | 553 | this.drawAxis(); 554 | 555 | if (this.options.show_vertical_labels) { 556 | this.drawVerticalLabels(); 557 | } 558 | 559 | if (this.options.show_horizontal_labels) { 560 | this.drawHorizontalLabels(); 561 | } 562 | 563 | if (this.grouped) { 564 | this.drawLines(key, null, this.normaliseData(this.flat_data)); 565 | } else { 566 | for (var key in this.data_sets) { 567 | if (this.data_sets.hasOwnProperty(key)) { 568 | var data = this.data_sets[key]; 569 | this.drawLines(key, this.options.colours[key], this.normaliseData(data)); 570 | } 571 | } 572 | } 573 | 574 | if (this.start_value !== 0) { 575 | this.drawFocusHint(); 576 | } 577 | }, 578 | 579 | drawGrid: function() { 580 | var pathString = '', i, y; 581 | 582 | if (this.options.show_vertical_labels) { 583 | y = this.graph_height + this.y_padding_top; 584 | for (i = 0; i < this.y_label_count; i++) { 585 | y = y - (this.graph_height / this.y_label_count); 586 | pathString += 'M' + this.x_padding_left + ',' + y; 587 | pathString += 'L' + (this.x_padding_left + this.graph_width) + ',' + y; 588 | } 589 | } 590 | 591 | if (this.options.show_horizontal_labels) { 592 | var x = this.x_padding_left + this.options.plot_padding + this.grid_start_offset, 593 | x_labels = this.grouped ? this.flat_data.length : this.options.labels.length, 594 | i, 595 | step = this.grid_step || this.step; 596 | 597 | for (i = 0; i < x_labels; i++) { 598 | pathString += 'M' + x + ',' + this.y_padding_top; 599 | pathString += 'L' + x +',' + (this.y_padding_top + this.graph_height); 600 | x = x + step; 601 | } 602 | 603 | x = x - this.options.plot_padding - 1; 604 | pathString += 'M' + x + ',' + this.y_padding_top; 605 | pathString += 'L' + x + ',' + (this.y_padding_top + this.graph_height); 606 | } 607 | 608 | this.paper.path(pathString).attr({ stroke: this.options.grid_colour, 'stroke-width': '1px' }); 609 | }, 610 | 611 | drawLines: function(label, colour, data) { 612 | var coords = this.calculateCoords(data), 613 | pathString = '', 614 | i, x, y; 615 | 616 | for (i = 0; i < coords.length; i++) { 617 | x = coords[i][0] || 0; 618 | y = coords[i][1] || 0; 619 | 620 | if (this.grouped && this.options.colours) { 621 | colour = this.options.colours[i % this.group_size]; 622 | } 623 | 624 | this.plottedCoords.push([x + this.bar_padding, y]); 625 | pathString = this.drawPlot(i, pathString, x, y, colour); 626 | } 627 | 628 | this.paper.path(pathString).attr({ stroke: colour, 'stroke-width': this.options.stroke_width }); 629 | 630 | if (this.options.bar_labels) { 631 | this.drawBarMarkers(); 632 | } 633 | 634 | if (this.options.line) { 635 | this.additionalLine(label, colour, this.normaliseData(this.options.line)); 636 | } 637 | }, 638 | 639 | additionalLine: function(label, colour, data) { 640 | var coords = this.calculateCoords(data), 641 | pathString = '', 642 | i, x, y, step, lineWidth = 3; 643 | 644 | if (this.grouped) { 645 | step = this.step * (this.group_size - 1); 646 | } 647 | 648 | for (i = 0; i < coords.length; i++) { 649 | x = coords[i][0] || 0; 650 | y = coords[i][1] || 0; 651 | 652 | if (this.grouped) { 653 | x += (step * i) + this.roundValue(step / 2, 0); 654 | } 655 | x += lineWidth; 656 | 657 | pathString = Ico.LineGraph.prototype.drawPlot.apply(this, [i, pathString, this.roundValue(x, 0), y, colour]); 658 | } 659 | 660 | this.paper.path(pathString).attr({ stroke: '#ff0000', 'stroke-width': lineWidth + 'px' }); 661 | }, 662 | 663 | calculateCoords: function(data) { 664 | var x = this.x_padding_left + this.options.plot_padding - this.step, 665 | y_offset = (this.graph_height + this.y_padding_top) + this.normalise(this.start_value), 666 | y = 0, 667 | coords = [], 668 | i; 669 | 670 | for (i = 0; i < data.length; i++) { 671 | y = y_offset - data[i]; 672 | x = x + this.step; 673 | coords.push([x, y]); 674 | } 675 | 676 | return coords; 677 | }, 678 | 679 | drawFocusHint: function() { 680 | var length = 5, 681 | x = this.x_padding_left + (length / 2) - 1, 682 | y = this.options.height - this.y_padding_bottom, 683 | pathString = ''; 684 | 685 | pathString += 'M' + x + ',' + y; 686 | pathString += 'L' + (x - length) + ',' + (y - length); 687 | pathString += 'M' + x + ',' + (y - length); 688 | pathString += 'L' + (x - length) + ',' + (y - (length * 2)); 689 | this.paper.path(pathString).attr({ stroke: this.options.label_colour, 'stroke-width': 2 }); 690 | }, 691 | 692 | drawMeanLine: function(data) { 693 | var offset = Helpers.sum(data) / data.length, 694 | pathString = ''; 695 | 696 | pathString += 'M' + (this.x_padding_left - 1) + ',' + (this.options.height - this.y_padding_bottom - offset); 697 | pathString += 'L' + (this.graph_width + this.x_padding_left) + ',' + (this.options.height - this.y_padding_bottom - offset); 698 | this.paper.path(pathString).attr(this.options.meanline); 699 | }, 700 | 701 | drawAxis: function() { 702 | var pathString = ''; 703 | 704 | pathString += 'M' + (this.x_padding_left - 1) + ',' + (this.options.height - this.y_padding_bottom); 705 | pathString += 'L' + (this.graph_width + this.x_padding_left) + ',' + (this.options.height - this.y_padding_bottom); 706 | pathString += 'M' + (this.x_padding_left - 1) + ',' + (this.options.height - this.y_padding_bottom); 707 | pathString += 'L' + (this.x_padding_left - 1) + ',' + (this.y_padding_top); 708 | 709 | this.paper.path(pathString).attr({ stroke: this.options.label_colour }); 710 | }, 711 | 712 | makeValueLabels: function(steps) { 713 | var step = this.label_step, 714 | label = this.start_value, 715 | labels = []; 716 | 717 | for (var i = 0; i < steps; i++) { 718 | label = this.roundValue((label + step), 2); 719 | labels.push(label); 720 | } 721 | return labels; 722 | }, 723 | 724 | /* Axis label markers */ 725 | drawMarkers: function(labels, direction, step, start_offset, font_offsets, extra_font_options) { 726 | function x_offset(value) { 727 | return value * direction[0]; 728 | } 729 | 730 | function y_offset(value) { 731 | return value * direction[1]; 732 | } 733 | 734 | /* Start at the origin */ 735 | var x = this.x_padding_left - 1 + x_offset(start_offset), 736 | y = this.options.height - this.y_padding_bottom + y_offset(start_offset), 737 | pathString = '', 738 | i, 739 | font_options = {}; 740 | Helpers.extend(font_options, this.font_options); 741 | Helpers.extend(font_options, extra_font_options || {}); 742 | 743 | for (i = 0; i < labels.length; i++) { 744 | pathString += 'M' + x + ',' + y; 745 | if (typeof labels[i] !== 'undefined' && (labels[i] + '').length > 0) { 746 | pathString += 'L' + (x + y_offset(5)) + ',' + (y + x_offset(5)); 747 | this.paper.text(x + font_offsets[0], y - font_offsets[1], labels[i]).attr(font_options).toFront(); 748 | } 749 | x = x + x_offset(step); 750 | y = y + y_offset(step); 751 | } 752 | 753 | this.paper.path(pathString).attr({ stroke: this.options.label_colour }); 754 | }, 755 | 756 | drawVerticalLabels: function() { 757 | var y_step = this.graph_height / this.y_label_count; 758 | this.drawMarkers(this.value_labels, [0, -1], y_step, y_step, [-8, -2], { 'text-anchor': 'end' }); 759 | }, 760 | 761 | drawHorizontalLabels: function() { 762 | var step = this.snap_to_grid ? this.grid_step : this.step; 763 | this.drawMarkers(this.options.labels, [1, 0], step, this.options.plot_padding, [0, (this.options.font_size + 7) * -1]); 764 | } 765 | }); 766 | /** 767 | * The BarGraph class. 768 | * 769 | * Example: 770 | * 771 | * new Ico.BarGraph($('bargraph'), [100, 10, 90, 20, 80, 30]); 772 | * 773 | */ 774 | Ico.BarGraph = function() { this.initialize.apply(this, arguments); }; 775 | Helpers.extend(Ico.BarGraph.prototype, Ico.BaseGraph.prototype); 776 | Helpers.extend(Ico.BarGraph.prototype, { 777 | // Overridden to handle grouped bar graphs 778 | buildDataSets: function(data, options) { 779 | if (typeof data.length !== 'undefined') { 780 | if (typeof data[0].length !== 'undefined') { 781 | this.grouped = true; 782 | 783 | // TODO: Find longest? 784 | this.group_size = data[0].length; 785 | var o = {}, k, i = 0; 786 | for (k in options.labels) { 787 | k = options.labels[k]; 788 | o[k] = data[i]; 789 | i++; 790 | } 791 | return o; 792 | } else { 793 | return { 'one': data }; 794 | } 795 | } else { 796 | return data; 797 | } 798 | }, 799 | 800 | /** 801 | * Sensible defaults for BarGraph. 802 | */ 803 | chartDefaults: function() { 804 | return { plot_padding: 0 }; 805 | }, 806 | 807 | /** 808 | * Ensures the normalises is always 0. 809 | */ 810 | normaliserOptions: function() { 811 | // Make sure the true largest value is used for max 812 | return this.options.line ? { start_value: 0, max: Helpers.max([Helpers.max(this.options.line), Helpers.max(this.flat_data)]) } : { start_value: 0 }; 813 | }, 814 | 815 | /** 816 | * Options specific to BarGraph. 817 | */ 818 | setChartSpecificOptions: function() { 819 | this.bar_padding = this.options.bar_padding || 5; 820 | this.bar_width = this.options.bar_size || this.calculateBarWidth(); 821 | 822 | if (this.options.bar_size && !this.options.bar_padding) { 823 | this.bar_padding = this.graph_width / this.data_size; 824 | } 825 | 826 | this.options.plot_padding = (this.bar_width / 2) - (this.bar_padding / 2); 827 | this.step = this.calculateStep(); 828 | this.grid_start_offset = this.bar_padding - 1; 829 | this.start_y = this.options.height - this.y_padding_bottom; 830 | }, 831 | 832 | /** 833 | * Calculates the width of each bar. 834 | * 835 | * @returns {Integer} The bar width 836 | */ 837 | calculateBarWidth: function() { 838 | var width = (this.graph_width / this.data_size) - this.bar_padding; 839 | 840 | if (this.grouped) { 841 | //width = width / this.group_size - (this.bar_padding * this.group_size); 842 | } 843 | 844 | if (this.options.max_bar_size && width > this.options.max_bar_size) { 845 | width = this.options.max_bar_size; 846 | this.bar_padding = this.graph_width / this.data_size; 847 | } 848 | 849 | return width; 850 | }, 851 | 852 | /** 853 | * Calculates step used to move from one bar to another. 854 | * 855 | * @returns {Float} The start value 856 | */ 857 | calculateStep: function() { 858 | return (this.graph_width - (this.options.plot_padding * 2) - (this.bar_padding * 2)) / validStepDivider(this.data_size); 859 | }, 860 | 861 | /** 862 | * Generates paths for Raphael. 863 | * 864 | * @param {Integer} index The index of the data value to plot 865 | * @param {String} pathString The pathString so far 866 | * @param {Integer} x The x-coord to plot 867 | * @param {Integer} y The y-coord to plot 868 | * @param {String} colour A string that represents a colour 869 | * @returns {String} The resulting path string 870 | */ 871 | drawPlot: function(index, pathString, x, y, colour) { 872 | if (this.options.highlight_colours && this.options.highlight_colours.hasOwnProperty(index)) { 873 | colour = this.options.highlight_colours[index]; 874 | } 875 | 876 | x = x + this.bar_padding; 877 | pathString += 'M' + x + ',' + this.start_y; 878 | pathString += 'L' + x + ',' + y; 879 | this.paper.path(pathString).attr({ stroke: colour, 'stroke-width': this.bar_width + 'px' }); 880 | pathString = ''; 881 | x = x + this.step; 882 | pathString += 'M' + x + ',' + this.start_y; 883 | return pathString; 884 | }, 885 | 886 | /* Change the standard options to correctly offset against the bars */ 887 | drawHorizontalLabels: function() { 888 | var x_start = this.bar_padding + this.options.plot_padding, 889 | step = this.step; 890 | if (this.grouped) { 891 | step = step * this.group_size; 892 | x_start = ((this.bar_width * this.group_size) + (this.bar_padding * this.group_size)) / 2 893 | x_start = this.roundValue(x_start, 0); 894 | } 895 | this.drawMarkers(this.options.labels, [1, 0], step, x_start, [0, (this.options.font_size + 7) * -1]); 896 | }, 897 | 898 | drawBarMarkers: function() { 899 | if (this.plottedCoords.length === 0) { 900 | return; 901 | } 902 | 903 | var i, length = this.flat_data.length, x, y, label, font_options = {}; 904 | Helpers.extend(font_options, this.font_options); 905 | font_options['text-anchor'] = 'center'; 906 | 907 | for (i = 0; i < length; i++) { 908 | label = this.roundValue(this.flat_data[i], 2).toString(); 909 | x = this.plottedCoords[i][0]; 910 | y = this.roundValue(this.plottedCoords[i][1], 0); 911 | this.paper.text(x, y - this.options.font_size, label).attr(font_options).toFront(); 912 | } 913 | } 914 | }); 915 | 916 | /** 917 | * Draws horizontal bar graphs. 918 | * 919 | * Example: 920 | * 921 | * new Ico.HorizontalBarGraph(element, 922 | * [2, 5, 1, 10, 15, 33, 20, 25, 1], 923 | * { font_size: 14 }); 924 | * 925 | */ 926 | Ico.HorizontalBarGraph = function() { this.initialize.apply(this, arguments); }; 927 | Helpers.extend(Ico.HorizontalBarGraph.prototype, Ico.BaseGraph.prototype); 928 | Helpers.extend(Ico.HorizontalBarGraph.prototype, { 929 | setChartSpecificOptions: function() { 930 | // Approximate the width required by the labels 931 | this.y_padding_top = 0; 932 | this.x_padding_left = 20 + this.longestLabel() * (this.options.font_size / 2); 933 | this.bar_padding = this.options.bar_padding || 5; 934 | this.bar_width = this.options.bar_size || this.calculateBarHeight(); 935 | 936 | if (this.options.bar_size && !this.options.bar_padding) { 937 | this.bar_padding = this.graph_height / this.data_size; 938 | } 939 | 940 | this.options.plot_padding = 0; 941 | this.step = this.calculateStep(); 942 | }, 943 | 944 | normalise: function(value) { 945 | var offset = this.x_padding_left; 946 | return ((value / this.range) * (this.graph_width - offset)); 947 | }, 948 | 949 | /* Height */ 950 | calculateBarHeight: function() { 951 | var height = (this.graph_height / this.data_size) - this.bar_padding; 952 | 953 | if (this.options.max_bar_size && height > this.options.max_bar_size) { 954 | height = this.options.max_bar_size; 955 | this.bar_padding = this.graph_height / this.data_size; 956 | } 957 | 958 | return height; 959 | }, 960 | 961 | calculateStep: function() { 962 | return (this.options.height - this.y_padding_bottom) / validStepDivider(this.data_size); 963 | }, 964 | 965 | drawLines: function(label, colour, data) { 966 | var x = this.x_padding_left + (this.options.plot_padding * 2), 967 | y = this.options.height - this.y_padding_bottom - (this.step / 2), 968 | pathString = 'M' + x + ',' + y, 969 | i; 970 | 971 | for (i = 0; i < data.length; i++) { 972 | pathString += 'L' + (x + data[i] - this.normalise(this.start_value)) + ',' + y; 973 | y = y - this.step; 974 | pathString += 'M' + x + ',' + y; 975 | } 976 | this.paper.path(pathString).attr({stroke: colour, 'stroke-width': this.bar_width + 'px'}); 977 | }, 978 | 979 | /* Horizontal version */ 980 | drawFocusHint: function() { 981 | var length = 5, 982 | x = this.x_padding_left + (this.step * 2), 983 | y = this.options.height - this.y_padding_bottom, 984 | pathString = ''; 985 | 986 | pathString += 'M' + x + ',' + y; 987 | pathString += 'L' + (x - length) + ',' + (y + length); 988 | pathString += 'M' + (x - length) + ',' + y; 989 | pathString += 'L' + (x - (length * 2)) + ',' + (y + length); 990 | this.paper.path(pathString).attr({stroke: this.options.label_colour, 'stroke-width': 2}); 991 | }, 992 | 993 | drawVerticalLabels: function() { 994 | var y_start = (this.step / 2) - (this.options.plot_padding * 2); 995 | this.drawMarkers(this.options.labels, [0, -1], this.step, y_start, [-8, (this.options.font_size / 8)], { 'text-anchor': 'end' }); 996 | }, 997 | 998 | drawHorizontalLabels: function() { 999 | var x_step = this.graph_width / this.y_label_count, 1000 | x_labels = this.makeValueLabels(this.y_label_count); 1001 | this.drawMarkers(x_labels, [1, 0], x_step, x_step, [0, (this.options.font_size + 7) * -1]); 1002 | } 1003 | }); 1004 | 1005 | /** 1006 | * Draws line graphs. 1007 | * 1008 | * Example: 1009 | * 1010 | * new Ico.LineGraph(element, [10, 5, 22, 44, 4]); 1011 | * 1012 | */ 1013 | Ico.LineGraph = function() { this.initialize.apply(this, arguments); }; 1014 | Helpers.extend(Ico.LineGraph.prototype, Ico.BaseGraph.prototype); 1015 | Helpers.extend(Ico.LineGraph.prototype, { 1016 | normalise: function(value) { 1017 | if (value === 0) { 1018 | return 0; 1019 | } 1020 | 1021 | var total = this.start_value === 0 ? this.top_value : this.top_value - this.start_value; 1022 | return ((value / total) * (this.graph_height)); 1023 | }, 1024 | 1025 | chartDefaults: function() { 1026 | return { plot_padding: 10, stroke_width: '3px' }; 1027 | }, 1028 | 1029 | setChartSpecificOptions: function() { 1030 | // Approximate the width required by the labels 1031 | var longestLabel = this.longestLabel(this.value_labels); 1032 | this.x_padding_left = 30 + longestLabel * (this.options.font_size / 2); 1033 | 1034 | if (typeof this.options.curve_amount === 'undefined') { 1035 | this.options.curve_amount = 10; 1036 | } 1037 | }, 1038 | 1039 | normaliserOptions: function() { 1040 | return { start_value: this.options.start_value }; 1041 | }, 1042 | 1043 | calculateStep: function() { 1044 | return this.graph_width / this.data_size; 1045 | }, 1046 | 1047 | drawPlot: function(index, pathString, x, y, colour) { 1048 | var w = this.options.curve_amount; 1049 | 1050 | if (this.options.markers === 'circle') { 1051 | var circle = this.paper.circle(x, y, this.options.marker_size); 1052 | circle.attr({ 'stroke-width': '1px', stroke: this.options.background_colour, fill: colour }); 1053 | } 1054 | 1055 | if (index === 0) { 1056 | this.lastPoint = { x: x, y: y }; 1057 | return pathString + 'M' + x + ',' + y; 1058 | } 1059 | 1060 | if (w) { 1061 | pathString += ['C', this.lastPoint.x + w, this.lastPoint.y, x - w, y, x, y]; 1062 | } else { 1063 | pathString += 'L' + x + ',' + y; 1064 | } 1065 | 1066 | this.lastPoint = { x: x, y: y }; 1067 | return pathString; 1068 | } 1069 | }); 1070 | 1071 | /** 1072 | * Draws spark line graphs. 1073 | * 1074 | * Example: 1075 | * 1076 | * new Ico.SparkLine(element, 1077 | * [21, 41, 32, 1, 10, 5, 32, 10, 23], 1078 | * { width: 30, height: 14, 1079 | * background_colour: '#ccc' }); 1080 | * 1081 | * 1082 | */ 1083 | Ico.SparkLine = function() { this.initialize.apply(this, arguments); }; 1084 | Ico.SparkLine.prototype = { 1085 | initialize: function(element, data, options) { 1086 | this.element = element; 1087 | this.data = data; 1088 | this.options = { 1089 | width: parseInt(getStyle(element, 'width'), 10), 1090 | height: parseInt(getStyle(element, 'height'), 10), 1091 | highlight: false, 1092 | background_colour: getStyle(element, 'backgroundColor') || '#ffffff', 1093 | colour: '#036' 1094 | }; 1095 | Helpers.extend(this.options, options || { }); 1096 | 1097 | this.step = this.calculateStep(); 1098 | this.paper = Raphael(this.element, this.options.width, this.options.height); 1099 | 1100 | if (this.options.acceptable_range) { 1101 | this.background = this.paper.rect(0, this.options.height - this.normalise(this.options.acceptable_range[1]), 1102 | this.options.width, 1103 | this.options.height - this.normalise(this.options.acceptable_range[0])); 1104 | } else { 1105 | this.background = this.paper.rect(0, 0, this.options.width, this.options.height); 1106 | } 1107 | 1108 | this.background.attr({fill: this.options.background_colour, stroke: 'none' }); 1109 | this.draw(); 1110 | }, 1111 | 1112 | calculateStep: function() { 1113 | return this.options.width / validStepDivider(this.data.length); 1114 | }, 1115 | 1116 | normalise: function(value) { 1117 | return (this.options.height / Helpers.max(this.data)) * value; 1118 | }, 1119 | 1120 | draw: function() { 1121 | var data = this.normaliseData(this.data); 1122 | this.drawLines('', this.options.colour, data); 1123 | 1124 | if (this.options.highlight) { 1125 | this.showHighlight(data); 1126 | } 1127 | }, 1128 | 1129 | drawLines: function(label, colour, data) { 1130 | var pathString = '', 1131 | x = 0, 1132 | values = data.slice(1), 1133 | i = 0; 1134 | 1135 | pathString = 'M0,' + (this.options.height - data[0]); 1136 | for (i = 1; i < data.length; i++) { 1137 | x = x + this.step; 1138 | pathString += 'L' + x +',' + Ico.round(this.options.height - data[i], 2); 1139 | } 1140 | this.paper.path(pathString).attr({stroke: colour}); 1141 | this.lastPoint = { x: 0, y: this.options.height - data[0] }; 1142 | }, 1143 | 1144 | showHighlight: function(data) { 1145 | var size = 2, 1146 | x = this.options.width - size, 1147 | i = this.options.highlight.index || data.length - 1, 1148 | y = data[i] + (Math.round(size / 2)); 1149 | 1150 | if (typeof(this.options.highlight.index) !== 'undefined') { 1151 | x = this.step * this.options.highlight.index; 1152 | } 1153 | 1154 | var circle = this.paper.circle(x, this.options.height - y, size); 1155 | circle.attr({ stroke: false, fill: this.options.highlight.colour}); 1156 | } 1157 | }; 1158 | Helpers.extend(Ico.SparkLine.prototype, Ico.Base); 1159 | 1160 | /** 1161 | * Draws spark bar graphs. 1162 | * 1163 | * Example: 1164 | * 1165 | * new Ico.SparkBar($('sparkline_2'), 1166 | * [1, 5, 10, 15, 20, 15, 10, 15, 30, 15, 10], 1167 | * { width: 30, height: 14, background_colour: '#ccc' }); 1168 | * 1169 | */ 1170 | Ico.SparkBar = function() { this.initialize.apply(this, arguments); }; 1171 | Helpers.extend(Ico.SparkBar.prototype, Ico.SparkLine.prototype); 1172 | Helpers.extend(Ico.SparkBar.prototype, { 1173 | calculateStep: function() { 1174 | return this.options.width / validStepDivider(this.data.length); 1175 | }, 1176 | 1177 | drawLines: function(label, colour, data) { 1178 | var width = this.step > 2 ? this.step - 1 : this.step, 1179 | x = width, 1180 | pathString = '', 1181 | i = 0; 1182 | for (i = 0; i < data.length; i++) { 1183 | pathString += 'M' + x + ',' + (this.options.height - data[i]); 1184 | pathString += 'L' + x + ',' + this.options.height; 1185 | x = x + this.step; 1186 | } 1187 | this.paper.path(pathString).attr({ stroke: colour, 'stroke-width': width }); 1188 | } 1189 | }); 1190 | 1191 | /** 1192 | * Assign the Ico object as a global property. 1193 | */ 1194 | global.Ico = Ico; 1195 | 1196 | if (typeof exports !== 'undefined') { 1197 | module.exports = Ico; 1198 | } 1199 | }(typeof window === 'undefined' ? this : window)); 1200 | 1201 | -------------------------------------------------------------------------------- /docs/ico.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Ico 3 | * Copyright (C) 2009-2011 Alex R. Young 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * The Ico object. 9 | */ 10 | (function(global) { 11 | var Ico = { 12 | VERSION: '0.3.10', 13 | 14 | /** 15 | * Rounds a float to the specified number of decimal places. 16 | * 17 | * @param {Float} num A number to round 18 | * @param {Integer} dec The number of decimal places 19 | * @returns {Float} The rounded result 20 | */ 21 | round: function(num, dec) { 22 | var result = Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec); 23 | return result; 24 | } 25 | }; 26 | 27 | /** 28 | * Determines if a value is valid as a 'step' value. 29 | * Steps are the increments between each bar or line. 30 | * 31 | * @param {Integer} value A number to test 32 | * @returns {Integer} A valid step value 33 | */ 34 | function validStepDivider(value) { 35 | return value > 1 ? value - 1 : 1; 36 | } 37 | 38 | /** 39 | * Gets a CSS style property. 40 | * 41 | * @param {Object} el A DOM element 42 | * @param {String} styleProp The name of a style property 43 | * @returns {Object} The style value 44 | */ 45 | function getStyle(el, styleProp) { 46 | if (typeof window === 'undefined') { 47 | return; 48 | } 49 | 50 | var style; 51 | if (el.currentStyle) { 52 | style = el.currentStyle[styleProp]; 53 | } else if (window.getComputedStyle) { 54 | style = document.defaultView.getComputedStyle(el, null).getPropertyValue(styleProp); 55 | } 56 | if (style && style.length === 0) { 57 | style = null; 58 | } 59 | return style; 60 | } 61 | 62 | var Helpers = {}; 63 | 64 | Helpers.sum = function(a) { 65 | var i, sum; 66 | for (i = 0, sum = 0; i < a.length; sum += a[i++]) {} 67 | return sum; 68 | }; 69 | 70 | if (typeof Array.prototype.max === 'undefined') { 71 | Helpers.max = function(a) { 72 | return Math.max.apply({}, a); 73 | }; 74 | } else { 75 | Helpers.max = function(a) { 76 | return a.max(); 77 | }; 78 | } 79 | 80 | if (typeof Array.prototype.min === 'undefined') { 81 | Helpers.min = function(a) { 82 | return Math.min.apply({}, a); 83 | }; 84 | } else { 85 | Helpers.min = function(a) { 86 | return a.min(); 87 | }; 88 | } 89 | 90 | Helpers.mean = function(a) { 91 | return Helpers.sum(a) / a.length; 92 | }; 93 | 94 | Helpers.variance = function(a) { 95 | var mean = Helpers.mean(a), 96 | variance = 0; 97 | for (var i = 0; i < a.length; i++) { 98 | variance += Math.pow(a[i] - mean, 2); 99 | } 100 | return variance / (a.length - 1); 101 | }; 102 | 103 | Helpers.standard_deviation = function(a) { 104 | return Math.sqrt(Helpers.variance(a)); 105 | }; 106 | 107 | if (typeof Object.extend === 'undefined') { 108 | Helpers.extend = function(destination, source) { 109 | for (var property in source) { 110 | if (source.hasOwnProperty(property)) { 111 | destination[property] = source[property]; 112 | } 113 | } 114 | return destination; 115 | }; 116 | } else { 117 | Helpers.extend = Object.extend; 118 | } 119 | 120 | if (Object.keys) { 121 | Helpers.keys = Object.keys; 122 | } else { 123 | Helpers.keys = function(o) { 124 | if (o !== Object(o)) { 125 | throw new TypeError('Object.keys called on non-object'); 126 | } 127 | 128 | var ret = [], p; 129 | for (p in o) { 130 | if (Object.prototype.hasOwnProperty.call(o,p)) { 131 | ret.push(p); 132 | } 133 | } 134 | return ret; 135 | } 136 | } 137 | /** 138 | * Normalises lists of values to fit inside a graph. 139 | * 140 | * @param {Array} data A list of values 141 | * @param {Object} options Can be used to set the `start_value` 142 | */ 143 | Ico.Normaliser = function(data, options) { 144 | this.options = { 145 | start_value: null 146 | }; 147 | 148 | if (typeof options !== 'undefined') { 149 | this.options = options; 150 | } 151 | 152 | this.min = Helpers.min(data); 153 | this.max = this.options.max || Helpers.max(data); 154 | this.standard_deviation = Helpers.standard_deviation(data); 155 | this.range = 0; 156 | this.step = this.labelStep(this.max - this.min); 157 | this.start_value = this.calculateStart(); 158 | this.process(); 159 | }; 160 | 161 | Ico.Normaliser.prototype = { 162 | /** 163 | * Calculates the start value. This is often 0. 164 | * @returns {Float} The start value 165 | */ 166 | calculateStart: function() { 167 | var min = typeof this.options.start_value !== 'undefined' && this.min >= 0 ? this.options.start_value : this.min, 168 | start_value = this.round(min, 1); 169 | 170 | /* This is a boundary condition */ 171 | if (this.min > 0 && start_value > this.min) { 172 | return 0; 173 | } 174 | 175 | if (this.min === this.max) { 176 | return 0; 177 | } 178 | 179 | return start_value; 180 | }, 181 | 182 | /* Given a value, this method rounds it to the nearest good value for an origin */ 183 | round: function(value, offset) { 184 | offset = offset || 1; 185 | var roundedValue = value; 186 | 187 | if (this.standard_deviation > 0.1) { 188 | var multiplier = Math.pow(10, -offset); 189 | roundedValue = Math.round(value * multiplier) / multiplier; 190 | 191 | if (roundedValue > this.min) { 192 | return this.round(value - this.step); 193 | } 194 | } 195 | return roundedValue; 196 | }, 197 | 198 | /** 199 | * Calculates the range and step values. 200 | */ 201 | process: function() { 202 | this.range = this.max - this.start_value; 203 | this.step = this.labelStep(this.range); 204 | }, 205 | 206 | /** 207 | * Calculates the label step value. 208 | * 209 | * @param {Float} value A value to convert to a label position 210 | * @returns {Float} The rounded label step result 211 | */ 212 | labelStep: function(value) { 213 | return Math.pow(10, Math.round((Math.log(value) / Math.LN10)) - 1); 214 | } 215 | }; 216 | 217 | 218 | /*! 219 | * Ico 220 | * Copyright (C) 2009-2011 Alex R. Young 221 | * MIT Licensed 222 | */ 223 | 224 | /** 225 | * The Ico.Base object which contains useful generic functions. 226 | */ 227 | Ico.Base = { 228 | /** 229 | * Runs this.normalise on each value. 230 | * 231 | * @param {Array} data Values to normalise 232 | * @returns {Array} Normalised values 233 | */ 234 | normaliseData: function(data) { 235 | var values = [], 236 | i = 0; 237 | for (i = 0; i < data.length; i++) { 238 | values.push(this.normalise(data[i])); 239 | } 240 | return values; 241 | }, 242 | 243 | /** 244 | * Flattens objects into an array. 245 | * 246 | * @param {Object} data Values to flatten 247 | * @returns {Array} Flattened values 248 | */ 249 | flatten: function(data) { 250 | var flat_data = []; 251 | 252 | if (typeof data.length === 'undefined') { 253 | if (typeof data === 'object') { 254 | for (var key in data) { 255 | if (data.hasOwnProperty(key)) 256 | flat_data = flat_data.concat(this.flatten(data[key])); 257 | } 258 | } else { 259 | return []; 260 | } 261 | } 262 | 263 | for (var i = 0; i < data.length; i++) { 264 | if (typeof data[i].length === 'number') { 265 | flat_data = flat_data.concat(this.flatten(data[i])); 266 | } else { 267 | flat_data.push(data[i]); 268 | } 269 | } 270 | return flat_data; 271 | }, 272 | 273 | /** 274 | * Handy method to produce an array of numbers. 275 | * 276 | * @param {Integer} start A number to start at 277 | * @param {Integer} end A number to end at 278 | * @returns {Array} An array of values 279 | */ 280 | makeRange: function(start, end, options) { 281 | var values = [], i; 282 | for (i = start; i < end; i++) { 283 | if (options && options.skip) { 284 | if (i % options.skip === 0) { 285 | values.push(i); 286 | } else { 287 | values.push(undefined); 288 | } 289 | } else { 290 | values.push(i); 291 | } 292 | } 293 | return values; 294 | } 295 | }; 296 | /** 297 | * Ico.BaseGraph is extended by most of the other graphs. It 298 | * uses a simple pattern with methods that can be overridden. 299 | */ 300 | Ico.BaseGraph = function() { this.initialize.apply(this, arguments); }; 301 | Helpers.extend(Ico.BaseGraph.prototype, Ico.Base); 302 | Helpers.extend(Ico.BaseGraph.prototype, { 303 | /** 304 | * This base class is used by the other graphs in Ico. 305 | * 306 | * Options: 307 | * 308 | * `width`: The width of the container element 309 | * `height`: The height of the container element 310 | * `labels`: The textual labels 311 | * `label_count`: The number of numerical labels to display 312 | * `label_step`: The value to increment each numerical label 313 | * `start_value`: The value to start plotting from (generally 0) 314 | * `bar_size`: Set the size for a bar in a bar chart 315 | * `max_bar_size`: Set the maximum size for a bar in a bar chart 316 | * 317 | * @param {Object} A DOM element 318 | * @param {Array|Object} Data to display 319 | * @param {Object} Options 320 | * 321 | */ 322 | initialize: function(element, data, options) { 323 | options = options || {}; 324 | 325 | this.element = element.length ? element[0] : element; 326 | this.data_sets = this.buildDataSets(data, options); 327 | this.flat_data = this.flatten(data); 328 | this.data_size = this.longestDataSetLength(); 329 | this.plottedCoords = []; 330 | 331 | /* If one colour is specified, map it to a compatible set */ 332 | if (options && options.colour) { 333 | if (!options.colours) options.colours = {}; 334 | for (var key in this.data_sets) { 335 | if (this.data_sets.hasOwnProperty(key)) 336 | options.colours[key] = options.colour; 337 | } 338 | } 339 | 340 | this.options = { 341 | width: parseInt(getStyle(element, 'width'), 10), 342 | height: parseInt(getStyle(element, 'height'), 10), 343 | plot_padding: 10, // Padding for the graph line/bar plots 344 | font_size: 10, // Label font size 345 | show_horizontal_labels: true, 346 | show_vertical_labels: true, 347 | background_colour: getStyle(element, 'backgroundColor') || '#ffffff', 348 | label_colour: '#666', // Label text colour 349 | markers: false, // false, circle 350 | marker_size: 5, 351 | meanline: false, 352 | grid: false, 353 | grid_colour: '#ccc', 354 | y_padding_top: 20, 355 | draw: true, 356 | bar_labels: false // Display values on the top of bars 357 | }; 358 | 359 | Helpers.extend(this.options, this.chartDefaults() || {}); 360 | Helpers.extend(this.options, options); 361 | 362 | this.font_options = { 'font': this.options.font_size + 'px "Arial"', stroke: 'none', fill: '#000' }; 363 | this.normaliser = new Ico.Normaliser(this.flat_data, this.normaliserOptions()); 364 | this.label_step = options.label_step || this.normaliser.step; 365 | 366 | this.range = this.normaliser.range; 367 | this.start_value = options.start_value || this.normaliser.start_value; 368 | 369 | /* Padding around the graph area to make room for labels */ 370 | this.x_padding_left = 10 + this.paddingLeftOffset(); 371 | this.x_padding_right = 20; 372 | this.x_padding = this.x_padding_left + this.x_padding_right; 373 | this.y_padding_top = this.options.y_padding_top; 374 | 375 | this.y_padding_bottom = 20 + this.paddingBottomOffset(); 376 | this.y_padding = this.y_padding_top + this.y_padding_bottom; 377 | 378 | this.graph_width = this.options.width - (this.x_padding); 379 | this.graph_height = this.options.height - (this.y_padding); 380 | 381 | this.step = this.calculateStep(); 382 | this.grid_step = this.step; 383 | 384 | if (this.options.labels) { 385 | if (!this.grouped) { 386 | this.grid_step = this.graph_width / this.options.labels.length; 387 | this.snap_to_grid = true; 388 | } 389 | } else { 390 | this.options.labels = this.makeRange(1, this.data_size + 1); 391 | } 392 | 393 | if (this.options.bar_labels) { 394 | // TODO: Improve this so extra padding is used instead 395 | this.range += this.normaliser.step; 396 | } 397 | 398 | /* Calculate how many labels are required */ 399 | if (options.label_count) { 400 | this.y_label_count = options.label_count; 401 | } else { 402 | if (this.range === 0) { 403 | this.y_label_count = 1; 404 | this.label_step = 1; 405 | } else { 406 | this.y_label_count = Math.ceil(this.range / this.label_step); 407 | if ((this.normaliser.min + (this.y_label_count * this.normaliser.step)) < this.normaliser.max) { 408 | this.y_label_count += 1; 409 | } 410 | } 411 | } 412 | 413 | this.value_labels = this.makeValueLabels(this.y_label_count); 414 | this.top_value = this.value_labels[this.value_labels.length - 1]; 415 | 416 | /* Grid control options */ 417 | this.grid_start_offset = -1; 418 | 419 | /* Drawing */ 420 | if (this.options.draw) { 421 | if (typeof this.options.colours === 'undefined') { 422 | this.options.colours = this.makeRandomColours(); 423 | } 424 | 425 | this.paper = Raphael(this.element, this.options.width, this.options.height); 426 | this.background = this.paper.rect(0, 0, this.options.width, this.options.height); 427 | this.background.attr({fill: this.options.background_colour, stroke: 'none' }); 428 | 429 | if (this.options.meanline === true) { 430 | this.options.meanline = { 'stroke-width': '2px', stroke: '#BBBBBB' }; 431 | } 432 | 433 | this.setChartSpecificOptions(); 434 | this.lastPoint = { x: 0, y: 0 }; 435 | this.draw(); 436 | } 437 | }, 438 | 439 | buildDataSets: function(data, options) { 440 | return (typeof data.length !== 'undefined') ? { 'one': data } : data; 441 | }, 442 | 443 | normaliserOptions: function() { 444 | return {}; 445 | }, 446 | 447 | chartDefaults: function() { 448 | /* Define in child class */ 449 | return {}; 450 | }, 451 | 452 | drawPlot: function(index, pathString, x, y, colour) { 453 | /* Define in child class */ 454 | }, 455 | 456 | calculateStep: function() { 457 | /* Define in child classes */ 458 | }, 459 | 460 | makeRandomColours: function() { 461 | var colours; 462 | if (this.grouped) { 463 | colours = []; 464 | // Colours are supplied as integers for groups, because there's no obvious way to associate the bar name 465 | for (var i = 0; i < this.group_size; i++) { 466 | colours.push(Raphael.hsb2rgb(Math.random(), 1, 0.75).hex); 467 | } 468 | } else { 469 | colours = {}; 470 | for (var key in this.data_sets) { 471 | if (!colours.hasOwnProperty(key)) 472 | colours[key] = Raphael.hsb2rgb(Math.random(), 1, 0.75).hex; 473 | } 474 | } 475 | return colours; 476 | }, 477 | 478 | longestDataSetLength: function() { 479 | if (this.grouped) { 480 | // Return the total number of grouped values rather than 481 | // the longest array of data 482 | return this.flat_data.length; 483 | } 484 | 485 | var length = 0; 486 | for (var key in this.data_sets) { 487 | if (this.data_sets.hasOwnProperty(key)) { 488 | length = this.data_sets[key].length > length ? this.data_sets[key].length : length; 489 | } 490 | } 491 | return length; 492 | }, 493 | 494 | roundValue: function(value, length) { 495 | var multiplier = Math.pow(10, length); 496 | value *= multiplier; 497 | value = Math.round(value) / multiplier; 498 | return value; 499 | }, 500 | 501 | roundValues: function(data, length) { 502 | var roundedData = [], i; 503 | for (i = 0; i < data.length; i++) { 504 | roundedData.push(this.roundValue(data[i], length)); 505 | } 506 | return roundedData; 507 | }, 508 | 509 | longestLabel: function(values) { 510 | var labels = Array.prototype.slice.call(values || this.options.labels, 0); 511 | if (labels.length) { 512 | return labels.sort(function(a, b) { return a.toString().length < b.toString().length; })[0].toString().length; 513 | } 514 | return 0; 515 | }, 516 | 517 | paddingLeftOffset: function() { 518 | /* Find the longest label and multiply it by the font size */ 519 | var data = this.roundValues(this.flat_data, 2), 520 | longest_label_length = 0; 521 | 522 | longest_label_length = data.sort(function(a, b) { 523 | return a.toString().length < b.toString().length; 524 | })[0].toString().length; 525 | 526 | longest_label_length = longest_label_length > 2 ? longest_label_length - 1 : longest_label_length; 527 | return 10 + (longest_label_length * this.options.font_size); 528 | }, 529 | 530 | paddingBottomOffset: function() { 531 | /* Find the longest label and multiply it by the font size */ 532 | return this.options.font_size; 533 | }, 534 | 535 | normalise: function(value) { 536 | if (value === 0) { 537 | return 0; 538 | } 539 | 540 | var total = this.start_value === 0 ? this.top_value : this.range; 541 | return ((value / total) * (this.graph_height)); 542 | }, 543 | 544 | draw: function() { 545 | if (this.options.grid) { 546 | this.drawGrid(); 547 | } 548 | 549 | if (this.options.meanline) { 550 | this.drawMeanLine(this.normaliseData(this.flat_data)); 551 | } 552 | 553 | this.drawAxis(); 554 | 555 | if (this.options.show_vertical_labels) { 556 | this.drawVerticalLabels(); 557 | } 558 | 559 | if (this.options.show_horizontal_labels) { 560 | this.drawHorizontalLabels(); 561 | } 562 | 563 | if (this.grouped) { 564 | this.drawLines(key, null, this.normaliseData(this.flat_data)); 565 | } else { 566 | for (var key in this.data_sets) { 567 | if (this.data_sets.hasOwnProperty(key)) { 568 | var data = this.data_sets[key]; 569 | this.drawLines(key, this.options.colours[key], this.normaliseData(data)); 570 | } 571 | } 572 | } 573 | 574 | if (this.start_value !== 0) { 575 | this.drawFocusHint(); 576 | } 577 | }, 578 | 579 | drawGrid: function() { 580 | var pathString = '', i, y; 581 | 582 | if (this.options.show_vertical_labels) { 583 | y = this.graph_height + this.y_padding_top; 584 | for (i = 0; i < this.y_label_count; i++) { 585 | y = y - (this.graph_height / this.y_label_count); 586 | pathString += 'M' + this.x_padding_left + ',' + y; 587 | pathString += 'L' + (this.x_padding_left + this.graph_width) + ',' + y; 588 | } 589 | } 590 | 591 | if (this.options.show_horizontal_labels) { 592 | var x = this.x_padding_left + this.options.plot_padding + this.grid_start_offset, 593 | x_labels = this.grouped ? this.flat_data.length : this.options.labels.length, 594 | i, 595 | step = this.grid_step || this.step; 596 | 597 | for (i = 0; i < x_labels; i++) { 598 | pathString += 'M' + x + ',' + this.y_padding_top; 599 | pathString += 'L' + x +',' + (this.y_padding_top + this.graph_height); 600 | x = x + step; 601 | } 602 | 603 | x = x - this.options.plot_padding - 1; 604 | pathString += 'M' + x + ',' + this.y_padding_top; 605 | pathString += 'L' + x + ',' + (this.y_padding_top + this.graph_height); 606 | } 607 | 608 | this.paper.path(pathString).attr({ stroke: this.options.grid_colour, 'stroke-width': '1px' }); 609 | }, 610 | 611 | drawLines: function(label, colour, data) { 612 | var coords = this.calculateCoords(data), 613 | pathString = '', 614 | i, x, y; 615 | 616 | for (i = 0; i < coords.length; i++) { 617 | x = coords[i][0] || 0; 618 | y = coords[i][1] || 0; 619 | 620 | if (this.grouped && this.options.colours) { 621 | colour = this.options.colours[i % this.group_size]; 622 | } 623 | 624 | this.plottedCoords.push([x + this.bar_padding, y]); 625 | pathString = this.drawPlot(i, pathString, x, y, colour); 626 | } 627 | 628 | this.paper.path(pathString).attr({ stroke: colour, 'stroke-width': this.options.stroke_width }); 629 | 630 | if (this.options.bar_labels) { 631 | this.drawBarMarkers(); 632 | } 633 | 634 | if (this.options.line) { 635 | this.additionalLine(label, colour, this.normaliseData(this.options.line)); 636 | } 637 | }, 638 | 639 | additionalLine: function(label, colour, data) { 640 | var coords = this.calculateCoords(data), 641 | pathString = '', 642 | i, x, y, step, lineWidth = 3; 643 | 644 | if (this.grouped) { 645 | step = this.step * (this.group_size - 1); 646 | } 647 | 648 | for (i = 0; i < coords.length; i++) { 649 | x = coords[i][0] || 0; 650 | y = coords[i][1] || 0; 651 | 652 | if (this.grouped) { 653 | x += (step * i) + this.roundValue(step / 2, 0); 654 | } 655 | x += lineWidth; 656 | 657 | pathString = Ico.LineGraph.prototype.drawPlot.apply(this, [i, pathString, this.roundValue(x, 0), y, colour]); 658 | } 659 | 660 | this.paper.path(pathString).attr({ stroke: '#ff0000', 'stroke-width': lineWidth + 'px' }); 661 | }, 662 | 663 | calculateCoords: function(data) { 664 | var x = this.x_padding_left + this.options.plot_padding - this.step, 665 | y_offset = (this.graph_height + this.y_padding_top) + this.normalise(this.start_value), 666 | y = 0, 667 | coords = [], 668 | i; 669 | 670 | for (i = 0; i < data.length; i++) { 671 | y = y_offset - data[i]; 672 | x = x + this.step; 673 | coords.push([x, y]); 674 | } 675 | 676 | return coords; 677 | }, 678 | 679 | drawFocusHint: function() { 680 | var length = 5, 681 | x = this.x_padding_left + (length / 2) - 1, 682 | y = this.options.height - this.y_padding_bottom, 683 | pathString = ''; 684 | 685 | pathString += 'M' + x + ',' + y; 686 | pathString += 'L' + (x - length) + ',' + (y - length); 687 | pathString += 'M' + x + ',' + (y - length); 688 | pathString += 'L' + (x - length) + ',' + (y - (length * 2)); 689 | this.paper.path(pathString).attr({ stroke: this.options.label_colour, 'stroke-width': 2 }); 690 | }, 691 | 692 | drawMeanLine: function(data) { 693 | var offset = Helpers.sum(data) / data.length, 694 | pathString = ''; 695 | 696 | pathString += 'M' + (this.x_padding_left - 1) + ',' + (this.options.height - this.y_padding_bottom - offset); 697 | pathString += 'L' + (this.graph_width + this.x_padding_left) + ',' + (this.options.height - this.y_padding_bottom - offset); 698 | this.paper.path(pathString).attr(this.options.meanline); 699 | }, 700 | 701 | drawAxis: function() { 702 | var pathString = ''; 703 | 704 | pathString += 'M' + (this.x_padding_left - 1) + ',' + (this.options.height - this.y_padding_bottom); 705 | pathString += 'L' + (this.graph_width + this.x_padding_left) + ',' + (this.options.height - this.y_padding_bottom); 706 | pathString += 'M' + (this.x_padding_left - 1) + ',' + (this.options.height - this.y_padding_bottom); 707 | pathString += 'L' + (this.x_padding_left - 1) + ',' + (this.y_padding_top); 708 | 709 | this.paper.path(pathString).attr({ stroke: this.options.label_colour }); 710 | }, 711 | 712 | makeValueLabels: function(steps) { 713 | var step = this.label_step, 714 | label = this.start_value, 715 | labels = []; 716 | 717 | for (var i = 0; i < steps; i++) { 718 | label = this.roundValue((label + step), 2); 719 | labels.push(label); 720 | } 721 | return labels; 722 | }, 723 | 724 | /* Axis label markers */ 725 | drawMarkers: function(labels, direction, step, start_offset, font_offsets, extra_font_options) { 726 | function x_offset(value) { 727 | return value * direction[0]; 728 | } 729 | 730 | function y_offset(value) { 731 | return value * direction[1]; 732 | } 733 | 734 | /* Start at the origin */ 735 | var x = this.x_padding_left - 1 + x_offset(start_offset), 736 | y = this.options.height - this.y_padding_bottom + y_offset(start_offset), 737 | pathString = '', 738 | i, 739 | font_options = {}; 740 | Helpers.extend(font_options, this.font_options); 741 | Helpers.extend(font_options, extra_font_options || {}); 742 | 743 | for (i = 0; i < labels.length; i++) { 744 | pathString += 'M' + x + ',' + y; 745 | if (typeof labels[i] !== 'undefined' && (labels[i] + '').length > 0) { 746 | pathString += 'L' + (x + y_offset(5)) + ',' + (y + x_offset(5)); 747 | this.paper.text(x + font_offsets[0], y - font_offsets[1], labels[i]).attr(font_options).toFront(); 748 | } 749 | x = x + x_offset(step); 750 | y = y + y_offset(step); 751 | } 752 | 753 | this.paper.path(pathString).attr({ stroke: this.options.label_colour }); 754 | }, 755 | 756 | drawVerticalLabels: function() { 757 | var y_step = this.graph_height / this.y_label_count; 758 | this.drawMarkers(this.value_labels, [0, -1], y_step, y_step, [-8, -2], { 'text-anchor': 'end' }); 759 | }, 760 | 761 | drawHorizontalLabels: function() { 762 | var step = this.snap_to_grid ? this.grid_step : this.step; 763 | this.drawMarkers(this.options.labels, [1, 0], step, this.options.plot_padding, [0, (this.options.font_size + 7) * -1]); 764 | } 765 | }); 766 | /** 767 | * The BarGraph class. 768 | * 769 | * Example: 770 | * 771 | * new Ico.BarGraph($('bargraph'), [100, 10, 90, 20, 80, 30]); 772 | * 773 | */ 774 | Ico.BarGraph = function() { this.initialize.apply(this, arguments); }; 775 | Helpers.extend(Ico.BarGraph.prototype, Ico.BaseGraph.prototype); 776 | Helpers.extend(Ico.BarGraph.prototype, { 777 | // Overridden to handle grouped bar graphs 778 | buildDataSets: function(data, options) { 779 | if (typeof data.length !== 'undefined') { 780 | if (typeof data[0].length !== 'undefined') { 781 | this.grouped = true; 782 | 783 | // TODO: Find longest? 784 | this.group_size = data[0].length; 785 | var o = {}, k, i = 0; 786 | for (k in options.labels) { 787 | k = options.labels[k]; 788 | o[k] = data[i]; 789 | i++; 790 | } 791 | return o; 792 | } else { 793 | return { 'one': data }; 794 | } 795 | } else { 796 | return data; 797 | } 798 | }, 799 | 800 | /** 801 | * Sensible defaults for BarGraph. 802 | */ 803 | chartDefaults: function() { 804 | return { plot_padding: 0 }; 805 | }, 806 | 807 | /** 808 | * Ensures the normalises is always 0. 809 | */ 810 | normaliserOptions: function() { 811 | // Make sure the true largest value is used for max 812 | return this.options.line ? { start_value: 0, max: Helpers.max([Helpers.max(this.options.line), Helpers.max(this.flat_data)]) } : { start_value: 0 }; 813 | }, 814 | 815 | /** 816 | * Options specific to BarGraph. 817 | */ 818 | setChartSpecificOptions: function() { 819 | this.bar_padding = this.options.bar_padding || 5; 820 | this.bar_width = this.options.bar_size || this.calculateBarWidth(); 821 | 822 | if (this.options.bar_size && !this.options.bar_padding) { 823 | this.bar_padding = this.graph_width / this.data_size; 824 | } 825 | 826 | this.options.plot_padding = (this.bar_width / 2) - (this.bar_padding / 2); 827 | this.step = this.calculateStep(); 828 | this.grid_start_offset = this.bar_padding - 1; 829 | this.start_y = this.options.height - this.y_padding_bottom; 830 | }, 831 | 832 | /** 833 | * Calculates the width of each bar. 834 | * 835 | * @returns {Integer} The bar width 836 | */ 837 | calculateBarWidth: function() { 838 | var width = (this.graph_width / this.data_size) - this.bar_padding; 839 | 840 | if (this.grouped) { 841 | //width = width / this.group_size - (this.bar_padding * this.group_size); 842 | } 843 | 844 | if (this.options.max_bar_size && width > this.options.max_bar_size) { 845 | width = this.options.max_bar_size; 846 | this.bar_padding = this.graph_width / this.data_size; 847 | } 848 | 849 | return width; 850 | }, 851 | 852 | /** 853 | * Calculates step used to move from one bar to another. 854 | * 855 | * @returns {Float} The start value 856 | */ 857 | calculateStep: function() { 858 | return (this.graph_width - (this.options.plot_padding * 2) - (this.bar_padding * 2)) / validStepDivider(this.data_size); 859 | }, 860 | 861 | /** 862 | * Generates paths for Raphael. 863 | * 864 | * @param {Integer} index The index of the data value to plot 865 | * @param {String} pathString The pathString so far 866 | * @param {Integer} x The x-coord to plot 867 | * @param {Integer} y The y-coord to plot 868 | * @param {String} colour A string that represents a colour 869 | * @returns {String} The resulting path string 870 | */ 871 | drawPlot: function(index, pathString, x, y, colour) { 872 | if (this.options.highlight_colours && this.options.highlight_colours.hasOwnProperty(index)) { 873 | colour = this.options.highlight_colours[index]; 874 | } 875 | 876 | x = x + this.bar_padding; 877 | pathString += 'M' + x + ',' + this.start_y; 878 | pathString += 'L' + x + ',' + y; 879 | this.paper.path(pathString).attr({ stroke: colour, 'stroke-width': this.bar_width + 'px' }); 880 | pathString = ''; 881 | x = x + this.step; 882 | pathString += 'M' + x + ',' + this.start_y; 883 | return pathString; 884 | }, 885 | 886 | /* Change the standard options to correctly offset against the bars */ 887 | drawHorizontalLabels: function() { 888 | var x_start = this.bar_padding + this.options.plot_padding, 889 | step = this.step; 890 | if (this.grouped) { 891 | step = step * this.group_size; 892 | x_start = ((this.bar_width * this.group_size) + (this.bar_padding * this.group_size)) / 2 893 | x_start = this.roundValue(x_start, 0); 894 | } 895 | this.drawMarkers(this.options.labels, [1, 0], step, x_start, [0, (this.options.font_size + 7) * -1]); 896 | }, 897 | 898 | drawBarMarkers: function() { 899 | if (this.plottedCoords.length === 0) { 900 | return; 901 | } 902 | 903 | var i, length = this.flat_data.length, x, y, label, font_options = {}; 904 | Helpers.extend(font_options, this.font_options); 905 | font_options['text-anchor'] = 'center'; 906 | 907 | for (i = 0; i < length; i++) { 908 | label = this.roundValue(this.flat_data[i], 2).toString(); 909 | x = this.plottedCoords[i][0]; 910 | y = this.roundValue(this.plottedCoords[i][1], 0); 911 | this.paper.text(x, y - this.options.font_size, label).attr(font_options).toFront(); 912 | } 913 | } 914 | }); 915 | 916 | /** 917 | * Draws horizontal bar graphs. 918 | * 919 | * Example: 920 | * 921 | * new Ico.HorizontalBarGraph(element, 922 | * [2, 5, 1, 10, 15, 33, 20, 25, 1], 923 | * { font_size: 14 }); 924 | * 925 | */ 926 | Ico.HorizontalBarGraph = function() { this.initialize.apply(this, arguments); }; 927 | Helpers.extend(Ico.HorizontalBarGraph.prototype, Ico.BaseGraph.prototype); 928 | Helpers.extend(Ico.HorizontalBarGraph.prototype, { 929 | setChartSpecificOptions: function() { 930 | // Approximate the width required by the labels 931 | this.y_padding_top = 0; 932 | this.x_padding_left = 20 + this.longestLabel() * (this.options.font_size / 2); 933 | this.bar_padding = this.options.bar_padding || 5; 934 | this.bar_width = this.options.bar_size || this.calculateBarHeight(); 935 | 936 | if (this.options.bar_size && !this.options.bar_padding) { 937 | this.bar_padding = this.graph_height / this.data_size; 938 | } 939 | 940 | this.options.plot_padding = 0; 941 | this.step = this.calculateStep(); 942 | }, 943 | 944 | normalise: function(value) { 945 | var offset = this.x_padding_left; 946 | return ((value / this.range) * (this.graph_width - offset)); 947 | }, 948 | 949 | /* Height */ 950 | calculateBarHeight: function() { 951 | var height = (this.graph_height / this.data_size) - this.bar_padding; 952 | 953 | if (this.options.max_bar_size && height > this.options.max_bar_size) { 954 | height = this.options.max_bar_size; 955 | this.bar_padding = this.graph_height / this.data_size; 956 | } 957 | 958 | return height; 959 | }, 960 | 961 | calculateStep: function() { 962 | return (this.options.height - this.y_padding_bottom) / validStepDivider(this.data_size); 963 | }, 964 | 965 | drawLines: function(label, colour, data) { 966 | var x = this.x_padding_left + (this.options.plot_padding * 2), 967 | y = this.options.height - this.y_padding_bottom - (this.step / 2), 968 | pathString = 'M' + x + ',' + y, 969 | i; 970 | 971 | for (i = 0; i < data.length; i++) { 972 | pathString += 'L' + (x + data[i] - this.normalise(this.start_value)) + ',' + y; 973 | y = y - this.step; 974 | pathString += 'M' + x + ',' + y; 975 | } 976 | this.paper.path(pathString).attr({stroke: colour, 'stroke-width': this.bar_width + 'px'}); 977 | }, 978 | 979 | /* Horizontal version */ 980 | drawFocusHint: function() { 981 | var length = 5, 982 | x = this.x_padding_left + (this.step * 2), 983 | y = this.options.height - this.y_padding_bottom, 984 | pathString = ''; 985 | 986 | pathString += 'M' + x + ',' + y; 987 | pathString += 'L' + (x - length) + ',' + (y + length); 988 | pathString += 'M' + (x - length) + ',' + y; 989 | pathString += 'L' + (x - (length * 2)) + ',' + (y + length); 990 | this.paper.path(pathString).attr({stroke: this.options.label_colour, 'stroke-width': 2}); 991 | }, 992 | 993 | drawVerticalLabels: function() { 994 | var y_start = (this.step / 2) - (this.options.plot_padding * 2); 995 | this.drawMarkers(this.options.labels, [0, -1], this.step, y_start, [-8, (this.options.font_size / 8)], { 'text-anchor': 'end' }); 996 | }, 997 | 998 | drawHorizontalLabels: function() { 999 | var x_step = this.graph_width / this.y_label_count, 1000 | x_labels = this.makeValueLabels(this.y_label_count); 1001 | this.drawMarkers(x_labels, [1, 0], x_step, x_step, [0, (this.options.font_size + 7) * -1]); 1002 | } 1003 | }); 1004 | 1005 | /** 1006 | * Draws line graphs. 1007 | * 1008 | * Example: 1009 | * 1010 | * new Ico.LineGraph(element, [10, 5, 22, 44, 4]); 1011 | * 1012 | */ 1013 | Ico.LineGraph = function() { this.initialize.apply(this, arguments); }; 1014 | Helpers.extend(Ico.LineGraph.prototype, Ico.BaseGraph.prototype); 1015 | Helpers.extend(Ico.LineGraph.prototype, { 1016 | normalise: function(value) { 1017 | if (value === 0) { 1018 | return 0; 1019 | } 1020 | 1021 | var total = this.start_value === 0 ? this.top_value : this.top_value - this.start_value; 1022 | return ((value / total) * (this.graph_height)); 1023 | }, 1024 | 1025 | chartDefaults: function() { 1026 | return { plot_padding: 10, stroke_width: '3px' }; 1027 | }, 1028 | 1029 | setChartSpecificOptions: function() { 1030 | // Approximate the width required by the labels 1031 | var longestLabel = this.longestLabel(this.value_labels); 1032 | this.x_padding_left = 30 + longestLabel * (this.options.font_size / 2); 1033 | 1034 | if (typeof this.options.curve_amount === 'undefined') { 1035 | this.options.curve_amount = 10; 1036 | } 1037 | }, 1038 | 1039 | normaliserOptions: function() { 1040 | return { start_value: this.options.start_value }; 1041 | }, 1042 | 1043 | calculateStep: function() { 1044 | return this.graph_width / this.data_size; 1045 | }, 1046 | 1047 | drawPlot: function(index, pathString, x, y, colour) { 1048 | var w = this.options.curve_amount; 1049 | 1050 | if (this.options.markers === 'circle') { 1051 | var circle = this.paper.circle(x, y, this.options.marker_size); 1052 | circle.attr({ 'stroke-width': '1px', stroke: this.options.background_colour, fill: colour }); 1053 | } 1054 | 1055 | if (index === 0) { 1056 | this.lastPoint = { x: x, y: y }; 1057 | return pathString + 'M' + x + ',' + y; 1058 | } 1059 | 1060 | if (w) { 1061 | pathString += ['C', this.lastPoint.x + w, this.lastPoint.y, x - w, y, x, y]; 1062 | } else { 1063 | pathString += 'L' + x + ',' + y; 1064 | } 1065 | 1066 | this.lastPoint = { x: x, y: y }; 1067 | return pathString; 1068 | } 1069 | }); 1070 | 1071 | /** 1072 | * Draws spark line graphs. 1073 | * 1074 | * Example: 1075 | * 1076 | * new Ico.SparkLine(element, 1077 | * [21, 41, 32, 1, 10, 5, 32, 10, 23], 1078 | * { width: 30, height: 14, 1079 | * background_colour: '#ccc' }); 1080 | * 1081 | * 1082 | */ 1083 | Ico.SparkLine = function() { this.initialize.apply(this, arguments); }; 1084 | Ico.SparkLine.prototype = { 1085 | initialize: function(element, data, options) { 1086 | this.element = element; 1087 | this.data = data; 1088 | this.options = { 1089 | width: parseInt(getStyle(element, 'width'), 10), 1090 | height: parseInt(getStyle(element, 'height'), 10), 1091 | highlight: false, 1092 | background_colour: getStyle(element, 'backgroundColor') || '#ffffff', 1093 | colour: '#036' 1094 | }; 1095 | Helpers.extend(this.options, options || { }); 1096 | 1097 | this.step = this.calculateStep(); 1098 | this.paper = Raphael(this.element, this.options.width, this.options.height); 1099 | 1100 | if (this.options.acceptable_range) { 1101 | this.background = this.paper.rect(0, this.options.height - this.normalise(this.options.acceptable_range[1]), 1102 | this.options.width, 1103 | this.options.height - this.normalise(this.options.acceptable_range[0])); 1104 | } else { 1105 | this.background = this.paper.rect(0, 0, this.options.width, this.options.height); 1106 | } 1107 | 1108 | this.background.attr({fill: this.options.background_colour, stroke: 'none' }); 1109 | this.draw(); 1110 | }, 1111 | 1112 | calculateStep: function() { 1113 | return this.options.width / validStepDivider(this.data.length); 1114 | }, 1115 | 1116 | normalise: function(value) { 1117 | return (this.options.height / Helpers.max(this.data)) * value; 1118 | }, 1119 | 1120 | draw: function() { 1121 | var data = this.normaliseData(this.data); 1122 | this.drawLines('', this.options.colour, data); 1123 | 1124 | if (this.options.highlight) { 1125 | this.showHighlight(data); 1126 | } 1127 | }, 1128 | 1129 | drawLines: function(label, colour, data) { 1130 | var pathString = '', 1131 | x = 0, 1132 | values = data.slice(1), 1133 | i = 0; 1134 | 1135 | pathString = 'M0,' + (this.options.height - data[0]); 1136 | for (i = 1; i < data.length; i++) { 1137 | x = x + this.step; 1138 | pathString += 'L' + x +',' + Ico.round(this.options.height - data[i], 2); 1139 | } 1140 | this.paper.path(pathString).attr({stroke: colour}); 1141 | this.lastPoint = { x: 0, y: this.options.height - data[0] }; 1142 | }, 1143 | 1144 | showHighlight: function(data) { 1145 | var size = 2, 1146 | x = this.options.width - size, 1147 | i = this.options.highlight.index || data.length - 1, 1148 | y = data[i] + (Math.round(size / 2)); 1149 | 1150 | if (typeof(this.options.highlight.index) !== 'undefined') { 1151 | x = this.step * this.options.highlight.index; 1152 | } 1153 | 1154 | var circle = this.paper.circle(x, this.options.height - y, size); 1155 | circle.attr({ stroke: false, fill: this.options.highlight.colour}); 1156 | } 1157 | }; 1158 | Helpers.extend(Ico.SparkLine.prototype, Ico.Base); 1159 | 1160 | /** 1161 | * Draws spark bar graphs. 1162 | * 1163 | * Example: 1164 | * 1165 | * new Ico.SparkBar($('sparkline_2'), 1166 | * [1, 5, 10, 15, 20, 15, 10, 15, 30, 15, 10], 1167 | * { width: 30, height: 14, background_colour: '#ccc' }); 1168 | * 1169 | */ 1170 | Ico.SparkBar = function() { this.initialize.apply(this, arguments); }; 1171 | Helpers.extend(Ico.SparkBar.prototype, Ico.SparkLine.prototype); 1172 | Helpers.extend(Ico.SparkBar.prototype, { 1173 | calculateStep: function() { 1174 | return this.options.width / validStepDivider(this.data.length); 1175 | }, 1176 | 1177 | drawLines: function(label, colour, data) { 1178 | var width = this.step > 2 ? this.step - 1 : this.step, 1179 | x = width, 1180 | pathString = '', 1181 | i = 0; 1182 | for (i = 0; i < data.length; i++) { 1183 | pathString += 'M' + x + ',' + (this.options.height - data[i]); 1184 | pathString += 'L' + x + ',' + this.options.height; 1185 | x = x + this.step; 1186 | } 1187 | this.paper.path(pathString).attr({ stroke: colour, 'stroke-width': width }); 1188 | } 1189 | }); 1190 | 1191 | /** 1192 | * Assign the Ico object as a global property. 1193 | */ 1194 | global.Ico = Ico; 1195 | 1196 | if (typeof exports !== 'undefined') { 1197 | module.exports = Ico; 1198 | } 1199 | }(typeof window === 'undefined' ? this : window)); 1200 | 1201 | --------------------------------------------------------------------------------