399 lines
12 KiB
JavaScript
399 lines
12 KiB
JavaScript
/**
|
|
* donutty // Create SVG donut charts with Javascript
|
|
* @author simeydotme <simey.me@gmail.com>
|
|
* @version 2.0.0
|
|
* @license MIT
|
|
* @link http://simey.me
|
|
* @preserve
|
|
*/
|
|
|
|
(function( doc, win ) {
|
|
|
|
var donutty,
|
|
namespace = "http://www.w3.org/2000/svg";
|
|
|
|
function isDefined( input ) {
|
|
return typeof input !== "undefined";
|
|
}
|
|
|
|
function float( input ) {
|
|
return parseFloat( input, 10 );
|
|
}
|
|
|
|
function truth( input ) {
|
|
return isDefined( input ) && ( input === true || input === "true" );
|
|
}
|
|
|
|
donutty = win.Donutty = function( el, options ) {
|
|
|
|
if ( el && typeof el === "string" ) {
|
|
this.$wrapper = doc.querySelectorAll( el )[0];
|
|
} else if ( doc.querySelector( `.${el.className}` ) ) {
|
|
this.$wrapper = el;
|
|
} else {
|
|
this.$wrapper = doc.body;
|
|
options = el;
|
|
}
|
|
|
|
if ( !this.$wrapper ) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
if ( !isDefined( options ) ) {
|
|
|
|
options = this.getOptionsFromTag();
|
|
|
|
}
|
|
|
|
this.state = {};
|
|
this.options = options || {};
|
|
this.options.min = isDefined( this.options.min ) ? float( this.options.min ) : 0;
|
|
this.options.max = isDefined( this.options.max ) ? float( this.options.max ) : 100;
|
|
this.options.value = isDefined( this.options.value ) ? float( this.options.value ) : 50;
|
|
this.options.round = isDefined( this.options.round ) ? truth( this.options.round ) : true;
|
|
this.options.circle = isDefined( this.options.circle ) ? truth( this.options.circle ) : true;
|
|
this.options.padding = isDefined( this.options.padding ) ? float( this.options.padding ) : 4;
|
|
this.options.radius = float( this.options.radius ) || 50;
|
|
this.options.thickness = float( this.options.thickness ) || 10;
|
|
this.options.bg = this.options.bg || "rgba(70, 130, 180, 0.15)";
|
|
this.options.color = this.options.color || "mediumslateblue";
|
|
this.options.transition = this.options.transition || "all 1.2s cubic-bezier(0.57, 0.13, 0.18, 0.98)";
|
|
this.options.text = isDefined( this.options.text ) ? this.options.text : false;
|
|
|
|
this.init();
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
donutty.prototype.getOptionsFromTag = function() {
|
|
|
|
return JSON.parse(JSON.stringify(this.$wrapper.dataset));
|
|
|
|
};
|
|
|
|
donutty.prototype.init = function() {
|
|
|
|
this.$wrapper.donutty = this;
|
|
|
|
var values;
|
|
|
|
// create the state object from the options,
|
|
// and then get the dash values for use in element creation
|
|
this.createState();
|
|
values = this.getDashValues();
|
|
|
|
this.createSvg();
|
|
this.createBg( values );
|
|
this.createDonut( values );
|
|
this.createText();
|
|
this.insertFragments( values );
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
donutty.prototype.createState = function() {
|
|
|
|
this.state.min = this.options.min;
|
|
this.state.max = this.options.max;
|
|
this.state.value = this.options.value;
|
|
this.state.bg = this.options.bg;
|
|
this.state.color = this.options.color;
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
donutty.prototype.createText = function() {
|
|
|
|
if ( typeof this.options.text === "function" ) {
|
|
|
|
this.$text = doc.createElement( "span" );
|
|
this.$text.setAttribute( "class", "donut-text" );
|
|
this.$text.style.opacity = 0;
|
|
this.updateText();
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
donutty.prototype.createBg = function( values ) {
|
|
|
|
this.$bg = doc.createElementNS( namespace, "circle" );
|
|
|
|
this.$bg.setAttribute( "cx", "50%" );
|
|
this.$bg.setAttribute( "cy", "50%" );
|
|
this.$bg.setAttribute( "r", this.options.radius );
|
|
this.$bg.setAttribute( "fill", "transparent" );
|
|
this.$bg.setAttribute( "stroke", this.state.bg );
|
|
this.$bg.setAttribute( "stroke-width", this.options.thickness + this.options.padding );
|
|
this.$bg.setAttribute( "stroke-dasharray", values.full * values.multiplier );
|
|
this.$bg.setAttribute( "class", "donut-bg" );
|
|
|
|
if ( this.options.round ) {
|
|
this.$bg.setAttribute( "stroke-linecap", "round" );
|
|
}
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
donutty.prototype.createDonut = function( values ) {
|
|
|
|
this.$donut = doc.createElementNS( namespace, "circle" );
|
|
|
|
this.$donut.setAttribute( "fill", "transparent" );
|
|
this.$donut.setAttribute( "cx", "50%" );
|
|
this.$donut.setAttribute( "cy", "50%" );
|
|
this.$donut.setAttribute( "r", this.options.radius );
|
|
this.$donut.setAttribute( "stroke", this.state.color );
|
|
this.$donut.setAttribute( "stroke-width", this.options.thickness );
|
|
this.$donut.setAttribute( "stroke-dashoffset", values.full );
|
|
this.$donut.setAttribute( "stroke-dasharray", values.full );
|
|
this.$donut.setAttribute( "class", "donut-fill" );
|
|
this.$donut.style.opacity = 0;
|
|
|
|
if ( this.options.round ) {
|
|
this.$donut.setAttribute( "stroke-linecap", "round" );
|
|
}
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
donutty.prototype.createSvg = function() {
|
|
|
|
var viewbox = this.options.radius * 2 + this.options.thickness + ( this.options.padding * 2 ),
|
|
rotateExtra = this.options.round ? this.options.thickness / 3 : 0,
|
|
rotate = this.options.circle ? 90 + rotateExtra : -225;
|
|
|
|
this.$html = doc.createDocumentFragment();
|
|
this.$svg = doc.createElementNS( namespace, "svg" );
|
|
|
|
this.$svg.setAttribute( "xmlns", namespace );
|
|
this.$svg.setAttribute( "viewbox", "0 0 " + viewbox + " " + viewbox );
|
|
this.$svg.setAttribute( "transform", "rotate( " + rotate +" )" );
|
|
this.$svg.setAttribute( "preserveAspectRatio", "xMidYMid meet" );
|
|
this.$svg.setAttribute( "class", "donut" );
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
donutty.prototype.insertFragments = function( values ) {
|
|
|
|
this.$svg.appendChild( this.$bg );
|
|
this.$svg.appendChild( this.$donut );
|
|
this.$html.appendChild( this.$svg );
|
|
|
|
if ( this.$text ) {
|
|
this.$html.appendChild( this.$text );
|
|
}
|
|
|
|
this.$wrapper.appendChild( this.$html );
|
|
|
|
// because of a strange bug in browsers not updating
|
|
// the "preserveAspectRatio" setting when applied programmatically,
|
|
// we need to essentially delete the DOM fragment, and then
|
|
// set the innerHTML of the parent so that it updates in browser.
|
|
this.$wrapper.innerHTML = this.$wrapper.innerHTML;
|
|
|
|
// and because we just destroyed the DOM fragment and all
|
|
// the references to it, we now set all those references again.
|
|
this.$svg = this.$wrapper.querySelector(".donut");
|
|
this.$bg = this.$wrapper.querySelector(".donut-bg");
|
|
this.$donut = this.$wrapper.querySelector(".donut-fill");
|
|
if ( this.$text ) {
|
|
this.$text = this.$wrapper.querySelector(".donut-text");
|
|
}
|
|
|
|
// now the references are re-set, we can go
|
|
// ahead and animate the element again.
|
|
this.animate( values.fill, values.full );
|
|
|
|
};
|
|
|
|
donutty.prototype.getDashValues = function() {
|
|
|
|
var circumference,
|
|
percentageFilled,
|
|
absoluteFilled,
|
|
multiplier;
|
|
|
|
multiplier = this.options.circle ? 1 : 0.75;
|
|
circumference = 2 * Math.PI * this.options.radius;
|
|
percentageFilled = ( this.state.value - this.state.min ) / ( this.state.max - this.state.min ) * 100;
|
|
absoluteFilled = circumference - ( ( circumference * multiplier ) / 100 * percentageFilled );
|
|
|
|
if (
|
|
this.options.round &&
|
|
this.options.circle &&
|
|
percentageFilled < 100 &&
|
|
absoluteFilled < this.options.thickness
|
|
) {
|
|
|
|
// when in circle mode, if the linecaps are "round"
|
|
// then the circle would look complete if it is actually
|
|
// only ~97% complete, this is because the linecaps
|
|
// overhang the stroke.
|
|
absoluteFilled = this.options.thickness;
|
|
|
|
}
|
|
|
|
return {
|
|
fill: absoluteFilled,
|
|
full: circumference,
|
|
multiplier: multiplier
|
|
};
|
|
|
|
};
|
|
|
|
donutty.prototype.animate = function( fill, full ) {
|
|
|
|
var _this = this;
|
|
|
|
// ensure the transition property is applied before
|
|
// the actual properties are set, so that browser renders
|
|
// the transition
|
|
_this.$bg.style.transition = this.options.transition;
|
|
_this.$donut.style.transition = this.options.transition;
|
|
if ( _this.$text ) {
|
|
_this.$text.style.transition = this.options.transition;
|
|
}
|
|
|
|
// use a short timeout (~60fps) to simulate a new
|
|
// animation frame (not using rAF due to ie9 problems)
|
|
window.setTimeout( function() {
|
|
|
|
_this.$bg.setAttribute( "stroke", _this.state.bg );
|
|
_this.$bg.style.opacity = 1;
|
|
|
|
_this.$donut.setAttribute( "stroke-dashoffset", fill );
|
|
_this.$donut.setAttribute( "stroke-dasharray", full );
|
|
_this.$donut.setAttribute( "stroke", _this.state.color );
|
|
_this.$donut.style.opacity = 1;
|
|
|
|
if ( _this.$text ) {
|
|
_this.$text.style.opacity = 1;
|
|
}
|
|
|
|
}, 16 );
|
|
|
|
};
|
|
|
|
/**
|
|
* use the current state to set the text inside
|
|
* the text element (only if option is provided);
|
|
* @return {object} the donut instance
|
|
*/
|
|
donutty.prototype.updateText = function() {
|
|
|
|
if ( typeof this.options.text === "function" ) {
|
|
|
|
this.$text.innerHTML = this.options.text( this.state );
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
/**
|
|
* set an individual state property for the chart
|
|
* @param {string} prop the property to set
|
|
* @param {string/number} val the value of the given property
|
|
* @return {object} the donut instance
|
|
* @chainable
|
|
*/
|
|
donutty.prototype.set = function( prop, val ) {
|
|
|
|
var values;
|
|
|
|
if ( isDefined( prop ) && isDefined( val ) ) {
|
|
|
|
this.state[ prop ] = val;
|
|
values = this.getDashValues();
|
|
this.updateText();
|
|
this.animate( values.fill, values.full );
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
/**
|
|
* set multiple state properties with an object
|
|
* @param {object} newState a map of properties to set
|
|
* @return {object} the donut instance
|
|
* @chainable
|
|
*/
|
|
donutty.prototype.setState = function( newState ) {
|
|
|
|
var values;
|
|
|
|
if ( isDefined( newState.value ) ) {
|
|
this.state.value = newState.value;
|
|
}
|
|
|
|
if ( isDefined( newState.min ) ) {
|
|
this.state.min = newState.min;
|
|
}
|
|
|
|
if ( isDefined( newState.max ) ) {
|
|
this.state.max = newState.max;
|
|
}
|
|
|
|
if ( isDefined( newState.bg ) ) {
|
|
this.state.bg = newState.bg;
|
|
}
|
|
|
|
if ( isDefined( newState.color ) ) {
|
|
this.state.color = newState.color;
|
|
}
|
|
|
|
values = this.getDashValues();
|
|
this.updateText();
|
|
this.animate( values.fill, values.full );
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
}( document, window ));
|
|
|
|
// jquery constructor
|
|
|
|
( function( Donutty, $ ) {
|
|
|
|
if ( typeof window.$ !== "undefined" ) {
|
|
|
|
$( function() {
|
|
|
|
$.fn.donutty = function( options ) {
|
|
|
|
return $( this ).each( function() {
|
|
|
|
new Donutty( this, options );
|
|
|
|
});
|
|
|
|
};
|
|
|
|
$( "[data-donutty]" ).donutty();
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
console.warn( "Can't find jQuery to attach Donutty" );
|
|
|
|
}
|
|
|
|
}( Donutty, jQuery ));
|