├── .gitignore ├── Gruntfile.js ├── MIT-LICENSE ├── README.md ├── bower.json ├── circles.js ├── circles.min.js ├── package.json └── spec ├── circle.html ├── circlesSpec.js ├── index.html ├── karma.conf.js ├── responsive.html └── viewport.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | .node_modules/* 3 | node_modules/* 4 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt) { 3 | 4 | // Project configuration. 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | 8 | meta: { 9 | banner: '/**\n' + 10 | ' * <%= pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' + 11 | ' *\n' + 12 | ' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' + 13 | ' * Licensed <%= pkg.license %>\n' + 14 | ' */\n' 15 | }, 16 | 17 | /** 18 | * Minify the sources! 19 | */ 20 | uglify: { 21 | compile: { 22 | options: { 23 | banner: '<%= meta.banner %>' 24 | }, 25 | files: { 26 | 'circles.min.js': ['circles.js'] 27 | } 28 | } 29 | }, 30 | 31 | jasmine: { 32 | pivotal: { 33 | src: 'circles.js', 34 | options: { 35 | specs: 'spec/*Spec.js', 36 | } 37 | } 38 | } 39 | 40 | 41 | }); 42 | 43 | // Dependencies 44 | grunt.loadNpmTasks('grunt-contrib-uglify'); 45 | grunt.loadNpmTasks('grunt-contrib-jasmine'); 46 | 47 | // Default task. 48 | grunt.registerTask('default', 'uglify'); 49 | 50 | 51 | 52 | }; 53 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2013 Artan Sinani 2 | http://www.lugolabs.com/circles 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following 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 OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Circles 2 | 3 | ![Circles](https://www.lugolabs.com/static/circles.png) 4 | 5 | Lightweight JavaScript library that generates circular graphs in SVG. Now with animation. 6 | 7 | ### Usage 8 | 9 | Include the `circles.js` or `circles.min.js` file in your HTML file. There are no dependencies. 10 | 11 | Create a placeholder div in your HTML: 12 | 13 | ```html 14 |
15 | ``` 16 | 17 | Create a graph by calling, the id should match id of the placeholder `
`: 18 | 19 | ```js 20 | var myCircle = Circles.create({ 21 | id: 'circles-1', 22 | radius: 60, 23 | value: 43, 24 | maxValue: 100, 25 | width: 10, 26 | text: function(value){return value + '%';}, 27 | colors: ['#D3B6C6', '#4B253A'], 28 | duration: 400, 29 | wrpClass: 'circles-wrp', 30 | textClass: 'circles-text', 31 | valueStrokeClass: 'circles-valueStroke', 32 | maxValueStrokeClass: 'circles-maxValueStroke', 33 | styleWrapper: true, 34 | styleText: true 35 | }); 36 | ``` 37 | 38 | where 39 | 40 | * `id` - the DOM element that will hold the graph 41 | * `radius` - the radius of the circles 42 | * `value` - init value of the circle (optional, defaults to 0) 43 | * `maxValue` - maximum value of the circle (optional, defaults to 100) 44 | * `width` - the width of the ring (optional, has value 10, if not specified) 45 | * `colors` - an array of colors, with the first item coloring the full circle (optional, it will be `['#EEE', '#F00']` if not specified) Can also be an rgba() value (example: ['rgba(255,255,255,0.25)', 'rgba(125,125,125,0.5)']) 46 | * `duration` - value in ms of animation's duration; defaults to 500; if 0 or `null` is passed, the animation will not run. 47 | * `wrpClass` - class name to apply on the generated element wrapping the whole circle. 48 | * `textClass` - class name to apply on the generated element wrapping the text content. 49 | * `valueStrokeClass` - class name to apply on the SVG path element which for the `value` amount. 50 | * `maxValueStrokeClass` - class name to apply on the SVG path element which for the `maxValue` amount. 51 | * `styleWrapper` - whether or not to add inline styles to the wrapper element (defaults to true) 52 | * `styleText` - whether or not to add inline styles to the text (defaults to true) 53 | * `text` - the text to display at the center of the graph (optional, the current "htmlified" value will be shown). If `null` or an empty string, no text will be displayed. It can also be a function: the returned value will be the displayed text like in the examples below: 54 | 55 | ```js 56 | // Example 1 57 | function(currentValue) { 58 | return '$'+currentValue; 59 | } 60 | // Example 2 61 | function() { 62 | return this.getPercent() + '%'; 63 | } 64 | ``` 65 | 66 | 67 | ### Install with grunt 68 | 69 | 1. Get the library 70 | 2. Install all the dependencies, run `npm install` 71 | 3. Once you have the dependencies installed, run `grunt` from the project directory. This will run the default grunt task which will minify `circles.js` to `circles.min.js` 72 | 73 | 74 | ### API 75 | 76 | ```js 77 | myCircle.updateRadius(Number radius) 78 | ``` 79 | 80 | Regenerates the circle with the given `radius` (see `spec/responsive.html` for an example on how to create a responsive circle). 81 | 82 | ```js 83 | myCircle.updateWidth(Number width) 84 | ``` 85 | 86 | Regenerates the circle with the given `width` 87 | 88 | ```js 89 | myCircle.updateColors(Array colors) 90 | ``` 91 | 92 | Change `colors` used to draw the circle. 93 | 94 | ```js 95 | myCircle.update(Number value [, Number duration]) 96 | ``` 97 | 98 | Set the value of the circle to `value`. 99 | Animation will take `duration` ms to execute. If no `duration` is given, default duration set in constructor will be used. 100 | If you don't want animation, set `duration` to 0. 101 | 102 | ```js 103 | myCircle.update(Boolean force) 104 | ``` 105 | 106 | Force the redrawing of the circle if `force` is set to **true**. Do nothing otherwise. 107 | 108 | ```js 109 | myCircle.getPercent() 110 | ``` 111 | 112 | Returns the percentage value of the circle, based on its current value and its max value. 113 | 114 | ```js 115 | myCircle.getValue() 116 | ``` 117 | 118 | Returns the value of the circle. 119 | 120 | ```js 121 | myCircle.getMaxValue() 122 | ``` 123 | 124 | Returns the max value of the circle. 125 | 126 | ```js 127 | myCircle.getValueFromPercent(Number percentage) 128 | ``` 129 | 130 | Returns the corresponding value of the circle based on its max value and given `percentage`. 131 | 132 | ```js 133 | myCircle.htmlifyNumber(Number number[, integerPartClass, decimalPartClass]) 134 | ``` 135 | 136 | Returned HTML representation of given `number` with given classes names applied on tags. 137 | Default value of `integerPartClass` is **circles-integer**. 138 | Default value of `decimalPartClass` is **circles-decimals**. 139 | 140 | ### Styles 141 | 142 | The styles have been specified inline to avoid external dependencies. But they can be overriden via CSS easily, being simply HTML elements. 143 | 144 | To help with this, a few CSS classes have been exposed: 145 | 146 | * `circles-wrp` - the element wrapping the whole circle 147 | * `circles-text` - the element wrapping text content 148 | * `circles-integer` - the element wrapping the text before the dot 149 | * `circles-decimals` - the element wrapping the decimal places 150 | 151 | You can override these classes by sending properties `wrpClass` and/or `textClass` to the constructor. 152 | 153 | 154 | ### Tests 155 | 156 | Tests can be run with [karma](http://karma-runner.github.io/0.12/index.html): 157 | 158 | ```shell 159 | karma start 160 | ``` 161 | 162 | or [grunt](http://gruntjs.com): 163 | 164 | ```shell 165 | grunt jasmine 166 | ``` 167 | 168 | ### Examples 169 | 170 | * [index.html](https://github.com/lugolabs/circles/blob/master/spec/index.html) - Overall functionality 171 | * [responsive.html](https://github.com/lugolabs/circles/blob/master/spec/responsive.html) - Making circles responsive 172 | * [viewport.html](https://github.com/lugolabs/circles/blob/master/spec/viewport.html) - Animate the circles when in viewport 173 | 174 | 175 | ### Compatibility 176 | 177 | Browsers that support SVG. 178 | 179 | **Desktop** 180 | * Firefox 3+ 181 | * Chrome 4+ 182 | * Safari 3.2+ 183 | * Opera 9+ 184 | * IE9 + 185 | 186 | **Mobile** 187 | * iOS Safari 3.2+ 188 | * Android Browser 3+ 189 | * Opera Mobile 10+ 190 | * Chrome for Android 18+ 191 | * Firefox for Android 15+ 192 | 193 | ### Contributors 194 | 195 | * Artan Sinani 196 | * [Adrien Guéret](https://github.com/adrien-gueret) 197 | * [radoslawhryciow](https://github.com/radoslawhryciow) 198 | * [Roman Salnikov](https://github.com/RSalo) 199 | * [webal](https://github.com/webal) 200 | * [Kieran](https://github.com/kieranajp) 201 | 202 | 203 | ### Inspirations 204 | 205 | Code and ideas borrowed by: 206 | 207 | * [Highcharts](http://highcharts.com) 208 | * Wout Fierens's [svg.js](http://svgjs.com) 209 | 210 | 211 | ### Licence 212 | 213 | Circles is licensed under the terms of the MIT License. 214 | 215 | ### Changelog 216 | 217 | * 0.0.6 Make inline styles optional. 218 | * 0.0.5 Rethink a bit the way the library is working + add some public methods. 219 | * 0.0.4 Exposes `generate(radius)` to regenerate the circle, opening it to responsiveness. 220 | * 0.0.3 Allow adding extra text to the graph (issue 3). 221 | Round integers during animation. 222 | * 0.0.2 Add animation. 223 | * 0.0.1 First release. 224 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "circles", 3 | "main": "circles.min.js", 4 | "version": "0.0.6", 5 | "homepage": "https://github.com/lugolabs/circles", 6 | "authors": [ 7 | "lugolabs" 8 | ], 9 | "description": "Lightweight JavaScript library that generates circular graphs in SVG. Now with animation.", 10 | "keywords": [ 11 | "circles", 12 | "js", 13 | "javascript", 14 | "svg" 15 | ], 16 | "license": "MIT", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower_components", 21 | "spec" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /circles.js: -------------------------------------------------------------------------------- 1 | // circles 2 | // copyright Artan Sinani 3 | // https://github.com/lugolabs/circles 4 | 5 | /* 6 | Lightwheight JavaScript library that generates circular graphs in SVG. 7 | 8 | Call Circles.create(options) with the following options: 9 | 10 | id - the DOM element that will hold the graph 11 | radius - the radius of the circles 12 | width - the width of the ring (optional, has value 10, if not specified) 13 | value - init value of the circle (optional, defaults to 0) 14 | maxValue - maximum value of the circle (optional, defaults to 100) 15 | text - the text to display at the centre of the graph (optional, the current "htmlified" value will be shown if not specified) 16 | if `null` or an empty string, no text will be displayed 17 | can also be a function: the returned value will be the displayed text 18 | ex1. function(currentValue) { 19 | return '$'+currentValue; 20 | } 21 | ex2. function() { 22 | return this.getPercent() + '%'; 23 | } 24 | colors - an array of colors, with the first item coloring the full circle 25 | (optional, it will be `['#EEE', '#F00']` if not specified) 26 | duration - value in ms of animation duration; (optional, defaults to 500); 27 | if 0 or `null` is passed, the animation will not run 28 | wrpClass - class name to apply on the generated element wrapping the whole circle. 29 | textClass: - class name to apply on the generated element wrapping the text content. 30 | 31 | API: 32 | updateRadius(radius) - regenerates the circle with the given radius (see spec/responsive.html for an example hot to create a responsive circle) 33 | updateWidth(width) - regenerates the circle with the given stroke width 34 | updateColors(colors) - change colors used to draw the circle 35 | update(value, duration) - update value of circle. If value is set to true, force the update of displaying 36 | getPercent() - returns the percentage value of the circle, based on its current value and its max value 37 | getValue() - returns the value of the circle 38 | getMaxValue() - returns the max value of the circle 39 | getValueFromPercent(percentage) - returns the corresponding value of the circle based on its max value and given percentage 40 | htmlifyNumber(number, integerPartClass, decimalPartClass) - returned HTML representation of given number with given classes names applied on tags 41 | 42 | */ 43 | 44 | (function(root, factory) { 45 | if(typeof exports === 'object') { 46 | module.exports = factory(); 47 | } 48 | else if(typeof define === 'function' && define.amd) { 49 | define([], factory); 50 | } 51 | else { 52 | root.Circles = factory(); 53 | } 54 | 55 | 56 | }(this, function() { 57 | 58 | "use strict"; 59 | 60 | var requestAnimFrame = window.requestAnimationFrame || 61 | window.webkitRequestAnimationFrame || 62 | window.mozRequestAnimationFrame || 63 | window.oRequestAnimationFrame || 64 | window.msRequestAnimationFrame || 65 | function (callback) { 66 | setTimeout(callback, 1000 / 60); 67 | }, 68 | 69 | Circles = function(options) { 70 | var elId = options.id; 71 | this._el = document.getElementById(elId); 72 | 73 | if (this._el === null) return; 74 | 75 | this._radius = options.radius || 10; 76 | this._duration = options.duration === undefined ? 500 : options.duration; 77 | 78 | this._value = 0.0000001; 79 | this._maxValue = options.maxValue || 100; 80 | 81 | this._text = options.text === undefined ? function(value){return this.htmlifyNumber(value);} : options.text; 82 | this._strokeWidth = options.width || 10; 83 | this._colors = options.colors || ['#EEE', '#F00']; 84 | this._svg = null; 85 | this._movingPath = null; 86 | this._wrapContainer = null; 87 | this._textContainer = null; 88 | 89 | this._wrpClass = options.wrpClass || 'circles-wrp'; 90 | this._textClass = options.textClass || 'circles-text'; 91 | 92 | this._valClass = options.valueStrokeClass || 'circles-valueStroke'; 93 | this._maxValClass = options.maxValueStrokeClass || 'circles-maxValueStroke'; 94 | 95 | this._styleWrapper = options.styleWrapper === false ? false : true; 96 | this._styleText = options.styleText === false ? false : true; 97 | 98 | var endAngleRad = Math.PI / 180 * 270; 99 | this._start = -Math.PI / 180 * 90; 100 | this._startPrecise = this._precise(this._start); 101 | this._circ = endAngleRad - this._start; 102 | 103 | this._generate().update(options.value || 0); 104 | }; 105 | 106 | Circles.prototype = { 107 | VERSION: '0.0.6', 108 | 109 | _generate: function() { 110 | 111 | this._svgSize = this._radius * 2; 112 | this._radiusAdjusted = this._radius - (this._strokeWidth / 2); 113 | 114 | this._generateSvg()._generateText()._generateWrapper(); 115 | 116 | this._el.innerHTML = ''; 117 | this._el.appendChild(this._wrapContainer); 118 | 119 | return this; 120 | }, 121 | 122 | _setPercentage: function(percentage) { 123 | this._movingPath.setAttribute('d', this._calculatePath(percentage, true)); 124 | this._textContainer.innerHTML = this._getText(this.getValueFromPercent(percentage)); 125 | }, 126 | 127 | _generateWrapper: function() { 128 | this._wrapContainer = document.createElement('div'); 129 | this._wrapContainer.className = this._wrpClass; 130 | 131 | if (this._styleWrapper) { 132 | this._wrapContainer.style.position = 'relative'; 133 | this._wrapContainer.style.display = 'inline-block'; 134 | } 135 | 136 | this._wrapContainer.appendChild(this._svg); 137 | this._wrapContainer.appendChild(this._textContainer); 138 | 139 | return this; 140 | }, 141 | 142 | _generateText: function() { 143 | 144 | this._textContainer = document.createElement('div'); 145 | this._textContainer.className = this._textClass; 146 | 147 | if (this._styleText) { 148 | var style = { 149 | position: 'absolute', 150 | top: 0, 151 | left: 0, 152 | textAlign: 'center', 153 | width: '100%', 154 | fontSize: (this._radius * .7) + 'px', 155 | height: this._svgSize + 'px', 156 | lineHeight: this._svgSize + 'px' 157 | }; 158 | 159 | for(var prop in style) { 160 | this._textContainer.style[prop] = style[prop]; 161 | } 162 | } 163 | 164 | this._textContainer.innerHTML = this._getText(0); 165 | return this; 166 | }, 167 | 168 | _getText: function(value) { 169 | if (!this._text) return ''; 170 | 171 | if (value === undefined) value = this._value; 172 | 173 | value = parseFloat(value.toFixed(2)); 174 | 175 | return typeof this._text === 'function' ? this._text.call(this, value) : this._text; 176 | }, 177 | 178 | _generateSvg: function() { 179 | 180 | this._svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 181 | this._svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); 182 | this._svg.setAttribute('width', this._svgSize); 183 | this._svg.setAttribute('height', this._svgSize); 184 | 185 | this._generatePath(100, false, this._colors[0], this._maxValClass)._generatePath(1, true, this._colors[1], this._valClass); 186 | 187 | this._movingPath = this._svg.getElementsByTagName('path')[1]; 188 | 189 | return this; 190 | }, 191 | 192 | _generatePath: function(percentage, open, color, pathClass) { 193 | var path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 194 | path.setAttribute('fill', 'transparent'); 195 | path.setAttribute('stroke', color); 196 | path.setAttribute('stroke-width', this._strokeWidth); 197 | path.setAttribute('d', this._calculatePath(percentage, open)); 198 | path.setAttribute('class', pathClass); 199 | 200 | this._svg.appendChild(path); 201 | 202 | return this; 203 | }, 204 | 205 | _calculatePath: function(percentage, open) { 206 | var end = this._start + ((percentage / 100) * this._circ), 207 | endPrecise = this._precise(end); 208 | return this._arc(endPrecise, open); 209 | }, 210 | 211 | _arc: function(end, open) { 212 | var endAdjusted = end - 0.001, 213 | longArc = end - this._startPrecise < Math.PI ? 0 : 1; 214 | 215 | return [ 216 | 'M', 217 | this._radius + this._radiusAdjusted * Math.cos(this._startPrecise), 218 | this._radius + this._radiusAdjusted * Math.sin(this._startPrecise), 219 | 'A', // arcTo 220 | this._radiusAdjusted, // x radius 221 | this._radiusAdjusted, // y radius 222 | 0, // slanting 223 | longArc, // long or short arc 224 | 1, // clockwise 225 | this._radius + this._radiusAdjusted * Math.cos(endAdjusted), 226 | this._radius + this._radiusAdjusted * Math.sin(endAdjusted), 227 | open ? '' : 'Z' // close 228 | ].join(' '); 229 | }, 230 | 231 | _precise: function(value) { 232 | return Math.round(value * 1000) / 1000; 233 | }, 234 | 235 | /*== Public methods ==*/ 236 | 237 | htmlifyNumber: function(number, integerPartClass, decimalPartClass) { 238 | 239 | integerPartClass = integerPartClass || 'circles-integer'; 240 | decimalPartClass = decimalPartClass || 'circles-decimals'; 241 | 242 | var parts = (number + '').split('.'), 243 | html = '' + parts[0]+''; 244 | 245 | if (parts.length > 1) { 246 | html += '.' + parts[1].substring(0, 2) + ''; 247 | } 248 | return html; 249 | }, 250 | 251 | updateRadius: function(radius) { 252 | this._radius = radius; 253 | 254 | return this._generate().update(true); 255 | }, 256 | 257 | updateWidth: function(width) { 258 | this._strokeWidth = width; 259 | 260 | return this._generate().update(true); 261 | }, 262 | 263 | updateColors: function(colors) { 264 | this._colors = colors; 265 | 266 | var paths = this._svg.getElementsByTagName('path'); 267 | 268 | paths[0].setAttribute('stroke', colors[0]); 269 | paths[1].setAttribute('stroke', colors[1]); 270 | 271 | return this; 272 | }, 273 | 274 | getPercent: function() { 275 | return (this._value * 100) / this._maxValue; 276 | }, 277 | 278 | getValueFromPercent: function(percentage) { 279 | return (this._maxValue * percentage) / 100; 280 | }, 281 | 282 | getValue: function() 283 | { 284 | return this._value; 285 | }, 286 | 287 | getMaxValue: function() 288 | { 289 | return this._maxValue; 290 | }, 291 | 292 | update: function(value, duration) { 293 | if (value === true) {//Force update with current value 294 | this._setPercentage(this.getPercent()); 295 | return this; 296 | } 297 | 298 | if (this._value == value || isNaN(value)) return this; 299 | if (duration === undefined) duration = this._duration; 300 | 301 | var self = this, 302 | oldPercentage = self.getPercent(), 303 | delta = 1, 304 | newPercentage, isGreater, steps, stepDuration; 305 | 306 | this._value = Math.min(this._maxValue, Math.max(0, value)); 307 | 308 | if (!duration) {//No duration, we can't skip the animation 309 | this._setPercentage(this.getPercent()); 310 | return this; 311 | } 312 | 313 | newPercentage = self.getPercent(); 314 | isGreater = newPercentage > oldPercentage; 315 | 316 | delta += newPercentage % 1; //If new percentage is not an integer, we add the decimal part to the delta 317 | steps = Math.floor(Math.abs(newPercentage - oldPercentage) / delta); 318 | stepDuration = duration / steps; 319 | 320 | 321 | (function animate(lastFrame) { 322 | if (isGreater) 323 | oldPercentage += delta; 324 | else 325 | oldPercentage -= delta; 326 | 327 | if ((isGreater && oldPercentage >= newPercentage) || (!isGreater && oldPercentage <= newPercentage)) 328 | { 329 | requestAnimFrame(function(){ self._setPercentage(newPercentage); }); 330 | return; 331 | } 332 | 333 | requestAnimFrame(function() { self._setPercentage(oldPercentage); }); 334 | 335 | var now = Date.now(), 336 | deltaTime = now - lastFrame; 337 | 338 | if (deltaTime >= stepDuration) { 339 | animate(now); 340 | } else { 341 | setTimeout(function() { 342 | animate(Date.now()); 343 | }, stepDuration - deltaTime); 344 | } 345 | 346 | })(Date.now()); 347 | 348 | return this; 349 | } 350 | }; 351 | 352 | Circles.create = function(options) { 353 | return new Circles(options); 354 | }; 355 | 356 | return Circles; 357 | })); 358 | -------------------------------------------------------------------------------- /circles.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * circles - v0.0.6 - 2018-03-07 3 | * 4 | * Copyright (c) 2018 lugolabs 5 | * Licensed 6 | */ 7 | !function(a,b){"object"==typeof exports?module.exports=b():"function"==typeof define&&define.amd?define([],b):a.Circles=b()}(this,function(){"use strict";var a=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){setTimeout(a,1e3/60)},b=function(a){var b=a.id;if(this._el=document.getElementById(b),null!==this._el){this._radius=a.radius||10,this._duration=void 0===a.duration?500:a.duration,this._value=1e-7,this._maxValue=a.maxValue||100,this._text=void 0===a.text?function(a){return this.htmlifyNumber(a)}:a.text,this._strokeWidth=a.width||10,this._colors=a.colors||["#EEE","#F00"],this._svg=null,this._movingPath=null,this._wrapContainer=null,this._textContainer=null,this._wrpClass=a.wrpClass||"circles-wrp",this._textClass=a.textClass||"circles-text",this._valClass=a.valueStrokeClass||"circles-valueStroke",this._maxValClass=a.maxValueStrokeClass||"circles-maxValueStroke",this._styleWrapper=!1!==a.styleWrapper,this._styleText=!1!==a.styleText;var c=Math.PI/180*270;this._start=-Math.PI/180*90,this._startPrecise=this._precise(this._start),this._circ=c-this._start,this._generate().update(a.value||0)}};return b.prototype={VERSION:"0.0.6",_generate:function(){return this._svgSize=2*this._radius,this._radiusAdjusted=this._radius-this._strokeWidth/2,this._generateSvg()._generateText()._generateWrapper(),this._el.innerHTML="",this._el.appendChild(this._wrapContainer),this},_setPercentage:function(a){this._movingPath.setAttribute("d",this._calculatePath(a,!0)),this._textContainer.innerHTML=this._getText(this.getValueFromPercent(a))},_generateWrapper:function(){return this._wrapContainer=document.createElement("div"),this._wrapContainer.className=this._wrpClass,this._styleWrapper&&(this._wrapContainer.style.position="relative",this._wrapContainer.style.display="inline-block"),this._wrapContainer.appendChild(this._svg),this._wrapContainer.appendChild(this._textContainer),this},_generateText:function(){if(this._textContainer=document.createElement("div"),this._textContainer.className=this._textClass,this._styleText){var a={position:"absolute",top:0,left:0,textAlign:"center",width:"100%",fontSize:.7*this._radius+"px",height:this._svgSize+"px",lineHeight:this._svgSize+"px"};for(var b in a)this._textContainer.style[b]=a[b]}return this._textContainer.innerHTML=this._getText(0),this},_getText:function(a){return this._text?(void 0===a&&(a=this._value),a=parseFloat(a.toFixed(2)),"function"==typeof this._text?this._text.call(this,a):this._text):""},_generateSvg:function(){return this._svg=document.createElementNS("http://www.w3.org/2000/svg","svg"),this._svg.setAttribute("xmlns","http://www.w3.org/2000/svg"),this._svg.setAttribute("width",this._svgSize),this._svg.setAttribute("height",this._svgSize),this._generatePath(100,!1,this._colors[0],this._maxValClass)._generatePath(1,!0,this._colors[1],this._valClass),this._movingPath=this._svg.getElementsByTagName("path")[1],this},_generatePath:function(a,b,c,d){var e=document.createElementNS("http://www.w3.org/2000/svg","path");return e.setAttribute("fill","transparent"),e.setAttribute("stroke",c),e.setAttribute("stroke-width",this._strokeWidth),e.setAttribute("d",this._calculatePath(a,b)),e.setAttribute("class",d),this._svg.appendChild(e),this},_calculatePath:function(a,b){var c=this._start+a/100*this._circ,d=this._precise(c);return this._arc(d,b)},_arc:function(a,b){var c=a-.001,d=a-this._startPrecise'+d[0]+"";return d.length>1&&(e+='.'+d[1].substring(0,2)+""),e},updateRadius:function(a){return this._radius=a,this._generate().update(!0)},updateWidth:function(a){return this._strokeWidth=a,this._generate().update(!0)},updateColors:function(a){this._colors=a;var b=this._svg.getElementsByTagName("path");return b[0].setAttribute("stroke",a[0]),b[1].setAttribute("stroke",a[1]),this},getPercent:function(){return 100*this._value/this._maxValue},getValueFromPercent:function(a){return this._maxValue*a/100},getValue:function(){return this._value},getMaxValue:function(){return this._maxValue},update:function(b,c){if(!0===b)return this._setPercentage(this.getPercent()),this;if(this._value==b||isNaN(b))return this;void 0===c&&(c=this._duration);var d,e,f,g,h=this,i=h.getPercent(),j=1;return this._value=Math.min(this._maxValue,Math.max(0,b)),c?(d=h.getPercent(),e=d>i,j+=d%1,f=Math.floor(Math.abs(d-i)/j),g=c/f,function b(c){if(e?i+=j:i-=j,e&&i>=d||!e&&i<=d)return void a(function(){h._setPercentage(d)});a(function(){h._setPercentage(i)});var f=Date.now(),k=f-c;k>=g?b(f):setTimeout(function(){b(Date.now())},g-k)}(Date.now()),this):(this._setPercentage(this.getPercent()),this)}},b.create=function(a){return new b(a)},b}); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "circles", 3 | "version": "0.0.6", 4 | "description": "Lightweight JavaScript library that generates circular graphs in SVG. Now with animation.", 5 | "main": "./circles.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/lugolabs/circles.git" 9 | }, 10 | "keywords": [ 11 | "circles", 12 | "js", 13 | "javascript", 14 | "svg" 15 | ], 16 | "author": "lugolabs", 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/chinchang/hint.css/blob/master/LICENSE-MIT" 21 | } 22 | ], 23 | "bugs": { 24 | "url": "https://github.com/lugolabs/circles/issues" 25 | }, 26 | "devDependencies": { 27 | "grunt": "~0.4.5", 28 | "grunt-contrib-jasmine": "^0.8.1", 29 | "grunt-contrib-uglify": "~0.4.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /spec/circle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Circles 4 | 14 | 15 | 16 |
17 | 18 | 19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /spec/circlesSpec.js: -------------------------------------------------------------------------------- 1 | describe('Circles', function() { 2 | it('is defined', function() { 3 | expect(typeof Circles).toEqual('function'); 4 | }); 5 | 6 | var element, circle; 7 | 8 | function getText() 9 | { 10 | return element.firstChild.children[1]; 11 | } 12 | 13 | function getSVG() 14 | { 15 | return element.firstChild.getElementsByTagName('svg')[0]; 16 | } 17 | 18 | beforeEach(function() { 19 | element = document.createElement('div'); 20 | element.id = 'circles-1'; 21 | document.body.appendChild(element); 22 | }); 23 | 24 | afterEach(function() { 25 | element.parentNode.removeChild(element); 26 | }); 27 | 28 | describe('Creation', function() { 29 | 30 | it('returns a Circles instance', function() { 31 | var circles = Circles.create({}); 32 | expect(circles instanceof Circles).toBeTruthy(); 33 | }); 34 | 35 | it('returns an instance with 0 as value', function() { 36 | var circles = Circles.create({id: element.id}); 37 | expect(circles.getValue()).toEqual(0); 38 | }); 39 | 40 | it('returns an instance with 100 as max value', function() { 41 | var circles = Circles.create({id: element.id}); 42 | expect(circles.getMaxValue()).toEqual(100); 43 | }); 44 | 45 | describe('Generated content', function() { 46 | 47 | beforeEach(function() { 48 | circle = Circles.create({ 49 | id: element.id, 50 | value: 40, 51 | radius: 60, 52 | duration: null 53 | }); 54 | }); 55 | 56 | it('contains a wrapper with default class', function() { 57 | 58 | Circles.create({ 59 | id: element.id, 60 | value: 40, 61 | radius: 60, 62 | duration: null, 63 | wrpClass: 'wrapContainer' 64 | }); 65 | 66 | expect(element.firstChild.className).toEqual('wrapContainer'); 67 | }); 68 | 69 | it('contains a wrapper with provided class', function() { 70 | expect(element.firstChild.className).toEqual('circles-wrp'); 71 | }); 72 | 73 | it('contains a SVG tag', function() { 74 | expect(getSVG() instanceof SVGSVGElement).toBeTruthy(); 75 | }); 76 | 77 | it('contains two PATH tags into the SVG', function() { 78 | expect(getSVG().getElementsByTagName('path').length).toEqual(2); 79 | }); 80 | 81 | it('adds class attributes to the path', function() { 82 | expect(getSVG().getElementsByTagName('path')[0].getAttribute('class')).toEqual('circles-maxValueStroke'); 83 | }); 84 | 85 | it('adds provided class attributes to the path', function() { 86 | 87 | Circles.create({ 88 | id: element.id, 89 | value: 40, 90 | radius: 60, 91 | duration: null, 92 | maxValueStrokeClass: 'testMaxValueClass', 93 | wrpClass: 'wrapContainer' 94 | }); 95 | 96 | expect(getSVG().getElementsByTagName('path')[0].getAttribute('class')).toEqual('testMaxValueClass'); 97 | }); 98 | 99 | it('contains the SVG without animation', function() { 100 | var dValue = getSVG().getElementsByTagName('path')[1].getAttribute('d'); 101 | expect(dValue).toEqual('M 59.988797973796764 5.000001140776291 A 55 55 0 0 1 92.3939094694214 104.44811165040569 '); 102 | }); 103 | 104 | it("contains the SVG with animation", function() { 105 | circle = Circles.create({ 106 | id: element.id, 107 | value: 40, 108 | radius: 60 109 | }); 110 | 111 | var dValue = element.firstChild.getElementsByTagName('svg')[0].getElementsByTagName('path')[1].getAttribute('d'); 112 | expect(dValue).toEqual('M 59.988797973796764 5.000001140776291 A 55 55 0 0 1 63.396635173034774 5.1049831997356705 '); 113 | }); 114 | 115 | it('has styles by default', function() { 116 | circle = Circles.create({ 117 | id: element.id, 118 | value: 40, 119 | radius: 60, 120 | text: '%', 121 | duration: null 122 | }); 123 | 124 | expect(element.firstChild.style[0]).toBeTruthy(); 125 | }); 126 | 127 | it('can have its styles overridden', function() { 128 | circle = Circles.create({ 129 | id: element.id, 130 | value: 40, 131 | radius: 60, 132 | duration: null, 133 | text: '%', 134 | styleWrapper: false 135 | }); 136 | 137 | expect(element.firstChild.style[0]).toBeFalsy(); 138 | }); 139 | 140 | }); 141 | 142 | describe('Text', function() { 143 | 144 | it('has a container', function() { 145 | circle = Circles.create({ 146 | id: element.id, 147 | value: 40, 148 | radius: 60, 149 | duration: null 150 | }); 151 | expect(getText() instanceof HTMLDivElement).toBeTruthy(); 152 | }); 153 | 154 | it('has a container with default class', function() { 155 | circle = Circles.create({ 156 | id: element.id, 157 | value: 40, 158 | radius: 60, 159 | duration: null 160 | }); 161 | 162 | expect(getText().className).toEqual('circles-text'); 163 | }); 164 | 165 | it('has a container with provided class', function() { 166 | circle = Circles.create({ 167 | id: element.id, 168 | value: 40, 169 | radius: 60, 170 | duration: null, 171 | textClass: 'textContainer' 172 | }); 173 | 174 | expect(getText().className).toEqual('textContainer'); 175 | }); 176 | 177 | it('contains the supplied text', function() { 178 | circle = Circles.create({ 179 | id: element.id, 180 | value: 40, 181 | radius: 60, 182 | text: '%', 183 | duration: null 184 | }); 185 | 186 | expect(getText().innerHTML).toEqual('%'); 187 | }); 188 | 189 | it('can be managed by a function', function() { 190 | circle = Circles.create({ 191 | id: element.id, 192 | value: 40, 193 | radius: 60, 194 | text: function(value) 195 | { 196 | return value + '%'; 197 | }, 198 | duration: null 199 | }); 200 | 201 | expect(getText().innerHTML).toEqual('40%'); 202 | }); 203 | 204 | it('can be empty', function() { 205 | circle = Circles.create({ 206 | id: element.id, 207 | value: 40, 208 | radius: 60, 209 | text: null, 210 | duration: null 211 | }); 212 | 213 | expect(getText().innerHTML).toBeFalsy(); 214 | }); 215 | 216 | it('has styles by default', function() { 217 | circle = Circles.create({ 218 | id: element.id, 219 | value: 40, 220 | radius: 60, 221 | text: '%', 222 | duration: null 223 | }); 224 | 225 | expect(getText().style[0]).toBeTruthy(); 226 | }); 227 | 228 | it('can have its styles overridden', function() { 229 | circle = Circles.create({ 230 | id: element.id, 231 | value: 40, 232 | radius: 60, 233 | duration: null, 234 | text: '%', 235 | styleText: false 236 | }); 237 | 238 | expect(getText().style[0]).toBeFalsy(); 239 | }); 240 | 241 | }); 242 | }); 243 | 244 | describe('API', function() { 245 | 246 | beforeEach(function() { 247 | circle = Circles.create({ 248 | id: element.id, 249 | value: 40, 250 | radius: 60, 251 | duration: null 252 | }); 253 | }); 254 | 255 | it('can update radius', function() { 256 | circle.updateRadius(30); 257 | 258 | expect(getSVG().getAttribute('width') == 60).toBeTruthy(); 259 | }); 260 | 261 | it('can update stroke width', function() { 262 | circle.updateWidth(20); 263 | 264 | expect(getSVG().getElementsByTagName('path')[1].getAttribute('stroke-width') == 20).toBeTruthy(); 265 | }); 266 | 267 | it('can update colors', function() { 268 | circle.updateColors(['#000', '#fff']); 269 | 270 | var paths = getSVG().getElementsByTagName('path'); 271 | 272 | expect(paths[0].getAttribute('stroke') === '#000' && paths[1].getAttribute('stroke') === '#fff').toBeTruthy(); 273 | }); 274 | 275 | it('can get correct percentage', function() { 276 | expect(circle.getPercent()).toEqual(40); 277 | }); 278 | 279 | it('can update value', function() { 280 | circle.update(50); 281 | expect(circle.getPercent()).toEqual(50); 282 | }); 283 | 284 | it('can get correct value from percentage', function() { 285 | 286 | circle = Circles.create({ 287 | id: element.id, 288 | maxValue: 1000, 289 | duration: null 290 | }); 291 | 292 | expect(circle.getValueFromPercent(25)).toEqual(250); 293 | }); 294 | 295 | it('can return HTML representation of a number', function() { 296 | expect(circle.htmlifyNumber(12.43, 'int', 'float')).toEqual('12.43'); 297 | }); 298 | }); 299 | 300 | }); 301 | -------------------------------------------------------------------------------- /spec/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Circles 4 | 28 | 29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 |
42 |
43 | 44 | 45 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /spec/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Dec 21 2013 19:04:41 GMT+0000 (GMT) 3 | 4 | 5 | // base path, that will be used to resolve files and exclude 6 | basePath = ''; 7 | 8 | 9 | // list of files / patterns to load in the browser 10 | files = [ 11 | JASMINE, 12 | JASMINE_ADAPTER, 13 | '../circles.js', 14 | '*Spec.js' 15 | ]; 16 | 17 | 18 | // list of files to exclude 19 | exclude = [ 20 | 21 | ]; 22 | 23 | 24 | // test results reporter to use 25 | // possible values: 'dots', 'progress', 'junit' 26 | reporters = ['progress']; 27 | 28 | 29 | // web server port 30 | port = 9876; 31 | 32 | 33 | // cli runner port 34 | runnerPort = 9100; 35 | 36 | 37 | // enable / disable colors in the output (reporters and logs) 38 | colors = true; 39 | 40 | 41 | // level of logging 42 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 43 | logLevel = LOG_INFO; 44 | 45 | 46 | // enable / disable watching file and executing tests whenever any file changes 47 | autoWatch = true; 48 | 49 | 50 | // Start these browsers, currently available: 51 | // - Chrome 52 | // - ChromeCanary 53 | // - Firefox 54 | // - Opera 55 | // - Safari (only Mac) 56 | // - PhantomJS 57 | // - IE (only Windows) 58 | browsers = ['Chrome']; 59 | 60 | 61 | // If browser does not capture in given timeout [ms], kill it 62 | captureTimeout = 60000; 63 | 64 | 65 | // Continuous Integration mode 66 | // if true, it capture browsers, run tests and exit 67 | singleRun = false; 68 | -------------------------------------------------------------------------------- /spec/responsive.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Circles 4 | 14 | 15 | 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | 57 | 58 | -------------------------------------------------------------------------------- /spec/viewport.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Circles 4 | 25 | 26 | 27 |

Example of animating the Circles only when visible

28 |

Scroll down to view them

29 |

30 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas dignissim eleifend justo a vestibulum. Quisque rutrum nec magna sit amet tristique. Donec magna magna, faucibus in enim id, malesuada volutpat justo. Etiam tellus tortor, aliquet vitae vestibulum quis, commodo auctor justo. Suspendisse pharetra egestas tempor. Ut tempus metus vel tincidunt mattis. Aliquam quis odio volutpat, interdum ante in, pulvinar purus. Aliquam auctor libero vitae ante vehicula efficitur. Integer eros nisi, dapibus eget varius in, aliquet finibus nisi. Phasellus tempus mauris ligula, ut luctus dolor volutpat id. Praesent lobortis nisl quam, a vehicula nibh malesuada quis. Curabitur malesuada sed metus ut pretium. Morbi commodo mauris nec fermentum tristique. Morbi pharetra rhoncus diam, ut gravida est laoreet maximus. 31 |

32 |

33 | Maecenas tellus metus, ultricies non ultrices vel, suscipit ac enim. Maecenas luctus molestie massa, id consequat lacus tempus vitae. In hac habitasse platea dictumst. Mauris hendrerit, tellus in cursus bibendum, nulla felis blandit erat, id sodales neque odio non tortor. Pellentesque a auctor leo. Mauris in velit nec est varius dignissim. Suspendisse eget ligula non lectus convallis imperdiet ut nec magna. Nam cursus odio non tortor tempor, at fermentum arcu tempus. Proin vitae laoreet nibh. In ullamcorper sem quis luctus sagittis. In consequat iaculis augue, ut consequat odio eleifend a. Duis imperdiet varius urna id faucibus. Curabitur suscipit purus vel sem sollicitudin, id dignissim dui consequat. 34 |

35 |

36 | Suspendisse nec velit viverra, pretium leo a, malesuada enim. Phasellus scelerisque ultricies tortor, eu faucibus leo laoreet eget. Morbi urna mi, tincidunt nec libero vitae, tempor hendrerit nisi. Nunc sed risus ipsum. Integer porttitor, nunc vitae vestibulum blandit, neque dolor consectetur mi, gravida euismod sem arcu eu elit. Praesent pretium odio id ipsum convallis, ut convallis est lobortis. In porttitor vulputate est ultricies volutpat. Mauris ornare eget nulla in gravida. Nunc laoreet fermentum risus, et fermentum leo suscipit vitae. Praesent tincidunt ligula eget quam pulvinar maximus. Maecenas at mollis nisi, non sagittis erat. 37 |

38 |

39 | Mauris quis vestibulum quam. Duis consequat sollicitudin tempor. Pellentesque cursus lacus ipsum, ut dictum nisl eleifend id. Nam quis nisl sodales, sodales nulla eu, placerat quam. Sed posuere mattis quam non condimentum. Pellentesque ut egestas nisl. Sed orci ante, laoreet quis sollicitudin sit amet, hendrerit at nisl. Curabitur vel dolor non nulla condimentum varius non nec orci. Sed tempor nisi erat, quis egestas enim tempor a. 40 |

41 |
42 |
43 |
44 |
45 |
46 |

47 | Sed aliquam felis nisl, elementum vehicula ipsum congue quis. Aenean sem sapien, rutrum et tincidunt sed, porta lacinia arcu. Nunc ut justo vel mi pharetra laoreet. Ut ornare ultrices pharetra. Ut rutrum orci pretium quam efficitur hendrerit. In aliquet laoreet est, non suscipit erat consequat eget. Quisque mollis rutrum tortor eget rutrum. Vivamus eu consequat diam, id lobortis justo. Phasellus tincidunt vel libero id pellentesque. Etiam leo justo, rhoncus ac mollis id, suscipit ac libero. 48 |

49 | 50 | 51 | 52 | 101 | 102 | 103 | --------------------------------------------------------------------------------