├── .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 | 
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 |
`:
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 | Change values to 20
38 | Change width to 20
39 | Change width to 10
40 | Swap colors
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 |
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 |
--------------------------------------------------------------------------------