/** * @license * Copyright (c) 2018, Immo Schulz-Gerlach, www.isg-software.de * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, this list * of conditions and the following disclaimer in the documentation and/or other materials * provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * Namespace of jQuery. Usually bound to the alias $. * * @see http://jquery.com/ * @namespace jQuery */ /** * Namespace for jQuery plug-ins. * * @see http://jquery.com/ * @namespace fn * @memberOf jQuery */ (function( $ ) { "use strict"; /** * Namespace for this jQuery plugin * @namespace progressPie * @memberOf jQuery.fn */ var idCounter = {}; function getValueInputObject(options) { let o = options.valueInput; if (typeof o === "object") { if (typeof o.val === "function") { return o; } else { throw new Error("option 'valueInput' is an object, but does not have a 'val' method, i.e. it's obviously not a jQuery result object."); } } else if (typeof o === "string") { return $(o); } else { return null; } } /** * Stores options for the progressPie plug-in. If this plug-in function is called, any succeeding calls to the progressPie plug-in * without argument will behave the same like when called with the options stored here. * This is recommended, if the progressPie plug-in gets called repeatedly (to update its graphic due to changed values). * Then, this setup provides the means to set the options only once and to keep update calls simple instead of * calling the progressPie repeatedly with the same options argument over and over again. *

The "update" option, if not specified in the options of this call, will default to true (regardless of * the value defined in $.fn.progressPie.defaults.

*

Usage pattern:

*
$(selector).setupProgressPie({options}).progressPie();
	 * update value;
	 * $(selector).progresssPie(); //update the graphic using the same options.
	 * 
*

Repeated calls of setupProgressPie are allowed and will update the options: The options of the subsequent call * get merged into the existing setup. Example: *


	 * $(selector).setupProgressPie({color: "green", strokeWidth: 3});
	 * ...
	 * $(selector).setupProgressPie({color: "navy"});
	 * 
*

In this example the second call will change the color for any following call of progressPie(), but * will leave the strokeWidth: 3 option untouched, i.e. will not reset it to the default.

*

Exception of this rule: You may add a second argument to the plugin call of type boolean. If you append * , true, the options will not be merged into an existing setup, but will completely overwrite * any existing setup like this was the first call of the setup method at all.

*

Suppose, in the example above, we change the second setup call to:

*
$(selector.setupProgressPie({color: "navy"}, true);
*

Then, this call will not only change the color from green to navy, but also reset the strokeWidth to default.

* @function setupProgressPie() * @memberOf jQuery.fn * @param {object} options - object containing individual options (merged with default options) * @param {boolean} replace - if true, the any previous setup will be completely replaced by this new setup. * Any property not configured in the options object will be reset to its default value (which may be either be undefined * or defined by the jQuery.fn.progressPie.defaults object). If false (or falsy, including null or undefined, i.e. * this also applies if you don't state an actual second argument at all), the passed options will be merged * into any already existing setup. * @return this / result set (for chainable method calls on the result set) */ $.fn.setupProgressPie = function(options, replace) { $(this).each(function() { var existingSetup = $(this).data($.fn.setupProgressPie.dataKey); if (replace || typeof existingSetup !== "object") { var opts = $.extend( {}, $.fn.progressPie.defaults, {update: true}, options ); $(this).data($.fn.setupProgressPie.dataKey, opts); } else { $.extend(existingSetup, options); } }); const opts = $.extend({}, $.fn.progressPie.defaults, options); const vi = getValueInputObject(opts); if (vi !== null) { if (typeof opts.valueInputEvents !== "string") { throw new Error("'valueInputEvents' has to be a string (space-separated list of event names)!"); } vi.on(opts.valueInputEvents, () => { $(this).progressPie(); }); } return this; }; $.fn.setupProgressPie.dataKey = "$.fn.setupProgressPie"; /** * This plug-in may be used to draw a piechart with only one filled pie (rest empty). * It is designed to be an alternative to a progress bar, since it simply depicts a single value (in percent). * The plug-in assumes that values in percent are part of the document (either visible or as attribute value) * and a small pie representing each value is to be dynamically inserted. This may be either a static display * or the pie may be updated upon data changes. * *

Typical application: Append or prepend to visible percent value:
* This mode assumes by default, that a value (integer number between 0 and 100 inclusive, * floating point numbers are supported, but truncated) is the only text content of an HTML element, e.g. a span * element, and the pie is to be prepended (or appended) to the same element. The pie will usually be auto-sized to fit * into the text line. A separator String to be placed between the pie and the number may be configured. * Defaults are to prepend the pie and use a non-breaking space as separator. * E.g. say you have HTML code like <p>You have achieved 25 points out of 50 (<span class="pie">50</span>%)</p>, * then you may insert a pie filled by 50% with $(.pie).progressPie();, resulting in a line like: * <p>You have achieved 25 points out of 50 (<span class="pie"><svg>the pie chart</svg>&nbsp;50</span>%)</p> * *

Usage: * Select the elements holding the percent number and to insert the pie into by a jQuery selector. * On the jQuery result set call "progressPie(options)", where options is an optional object * with configuration options. See Home (or README) for a documentation of supported options. * The plugin is applied to any element in the result set, i.e. if the selector did not found any matching * element, nothing will happen, while if the selector found several matching elements, the plugin * will try to insert a corresponding pie into each of the found elements individually. * *

The progressPie method will return this, enabling chaining of method calls * on the result set.

* * @function progressPie() * @memberOf jQuery.fn * @param options - object containing individual options (merged with default options) * @return this / result set (for chainable method calls on the result set) */ $.fn.progressPie = function( options ) { //Note: Normally the @function directive for jsDoc should not contain the parentheses "()". //But I needed to add something to the name in order to be able to document the plugin function and its namespace //separately (though in reality both are the same). /* property: either opts.margin or opts.padding, may be number or array. i \elem {0..3} => 0: top, 1: right, 2: bottom, 3: left */ function getMarginOrPaddingFromProp(property, i) { if (typeof property === 'number') { return property; } else if (Array.isArray(property) && property.length > 0) { if (property.length === 1) { return property[0]; } else { // length > 1 var j = i; while (j >= property.length) { // j > 1 ; j >= 2 j -= 2; // j >= 0 } // 0 <= j < length > 1 => array access valid return property[j]; } } else { return 0; } } const optsMethods = { getMargin: function(i) { return getMarginOrPaddingFromProp(this.margin, i); }, getPadding: function(i) { return getMarginOrPaddingFromProp(this.padding, i); } }; // Extend our default options with those provided. // Note that the first argument to extend is an empty // object – this is to keep from overriding our "defaults" object. var globalOpts = $.extend( {}, $.fn.progressPie.defaults, options, optsMethods ); var noargs = typeof options === "undefined"; //If noargs === true and the setupProgressPie plug-in has been called for a target element, don't use "globalOpts", but use the stored setup instead. //Since any element in the result set may have a different individual setup, this decision can't be made here globally, but has to be made individually in //the forEach loop below... var NS = "http://www.w3.org/2000/svg"; var contentPluginNS = "jQuery.fn.progressPie.contentPlugin"; //Naming convention: // "self" is used to refer to the function object in order to simplify access to public members. // "me", on the other hand, may be used in functions to save the object reference "this" (or "$(this)"). var self = $.fn.progressPie; var internalMode = $.extend( {USER_COLOR_CONST:{}, USER_COLOR_FUNC:{}, DATA_ATTR_FUNC:{}}, self.Mode ); //private functions function createId(prefix) { if (typeof idCounter[prefix] === "undefined") { idCounter[prefix] = 0; } return prefix + (++idCounter[prefix]); } function angle(percent) { return 0.02 * Math.PI * percent; //2 Pi * percent / 100 } function evalDataAttrFunc(functionString, percent) { var evalIndirect = eval; var handler = evalIndirect(functionString); if (typeof handler === "function") { return handler(percent); } else { throw new Error("The value of the colorFunctionAttr attribute is NOT a function: " + functionString); } } function evalContentPluginName(name) { var evalIndirect = eval; var f = evalIndirect(contentPluginNS + "." + name); if (typeof f === "function" || typeof f === 'object' && typeof f.draw === 'function') { return f; } else { throw new Error(name + " is not the name of a function or object in namespace " + contentPluginNS + "!"); } } function getContentPlugin(property) { var f; if (typeof property === 'function' || typeof property === 'object' && typeof property.draw === 'function') { f = property; } else if (typeof property === 'string') { f = evalContentPluginName(property); } else { throw new Error("contentPlugin option must either be a function or an object with method named 'draw' or the name of such a function or object in the namespace " + contentPluginNS + "!"); } return f; } function getContentPlugins(property) { if (Array.isArray(property)) { return $.map(property, getContentPlugin); } else { return [getContentPlugin(property)]; } } function getContentPluginOptions(property, i) { if (property === null) { return null; } else if (Array.isArray(property)) { return property[i]; } else if (i === 0 && typeof property === "object") { return property; } else { return null; } } function getArcLength(rad, percent) { return 0.02 * Math.PI * rad * percent; //2πr * percent/100 = 0.02πr * percent } function addAnimationFromTo(target, attrName, attrType, from, to, animationAttrs) { var anim = document.createElementNS(NS, "animate"); anim.setAttribute("attributeName", attrName); anim.setAttribute("attributeType", attrType); anim.setAttribute("from", from); anim.setAttribute("to", to); anim.setAttribute("fill", "freeze"); //when the animation stops, it's final state shall persist. for (var key in animationAttrs) { anim.setAttribute(key, animationAttrs[key]); } target.appendChild(anim); } function setStrokeDashArray(circle, strokeDashes, circumference) { var cnt; var len; if (typeof strokeDashes === 'number') { cnt = strokeDashes; } else if (typeof strokeDashes === 'object') { cnt = strokeDashes.count; len = strokeDashes.length; } else { throw new Error("illegal option: 'strokeDashes' is neither number (count) nor object!"); } if (typeof cnt === 'undefined') { throw new Error("illegal option: 'strokeDashes' does not specify the 'count' property!"); } if (typeof len === 'undefined') { //default: strokes and gaps equally long len = circumference / cnt / 2; } else if (typeof len === 'string') { len = len.trim(); var percent = len.substring(len.length - 1) === '%'; len = Number.parseInt(len, 10); if (percent) { len = circumference * len / 100; } } if (len * cnt >= circumference) { throw new Error("Illegal options: strokeDashCount * strokeDashLength >= circumference, can't set stroke-dasharray!"); } else { var gap = (circumference - len * cnt) / cnt; var offset = typeof strokeDashes === 'object' && strokeDashes.centered ? 1.0 * len / 2 : 0; if (typeof strokeDashes !== 'object' || !strokeDashes.inverted) { circle.style.strokeDasharray = "" + len + "px, " + gap + "px"; if (offset !== 0) { circle.style.strokeDashoffset = "" + offset + "px"; } } else { circle.style.strokeDasharray = "" + gap + "px, " + len + "px"; circle.style.strokeDashoffset = "" + (gap + offset) + "px"; } } } function addTitle(node, title) { if (typeof title === "string") { const t = document.createElementNS(NS, "title"); $(t).text(title); node.appendChild(t); } } function drawPie(svg, defs, rad, strokeWidth, strokeColor, strokeDashes, strokeFill, overlap, ringWidth, ringEndsRounded, ringAlign, cssClassBackgroundCircle, cssClassForegroundPie, percent, prevPercent, color, prevColor, title, animationAttrs, rotation) { //strokeWidth or ringWidth must not be greater than the radius: if (typeof strokeWidth === 'number') { strokeWidth = Math.min(strokeWidth, rad); } if (typeof ringWidth === 'number') { ringWidth = Math.min(ringWidth, rad); } var ringAlignRad = -1; if (typeof strokeWidth === 'number' && typeof ringWidth === 'number' && strokeWidth > 0 && ringWidth > 0 && strokeWidth !== ringWidth && overlap && (ringAlign === self.RingAlign.CENTER || ringAlign === self.RingAlign.INNER)) { //pre-calculate ringAlignRad for ring mode in case ringWidth and strokeWidth differ (and are both > 0) //and the ringAlign option INNER or CENTER is set. //This value ringAlignRad then denotes the radius for the "smaller" (with slimmer stroke) of the two circles. //Depending on whose stroke-width is smaller, this will either be applied to the background circle //or to the ring. This decision is made later. var maxw = Math.max(ringWidth, strokeWidth); var minw = Math.min(ringWidth, strokeWidth); ringAlignRad = ringAlign === self.RingAlign.CENTER ? rad - (maxw / 2) : rad - maxw + (minw / 2); } var r; var circle; var strokeColorConfigured = false; //default value //1. background Circle // (now always drawn, even with strokeWidth==0, with CSS class allowing // to set the CSS fill property for the background) // (not drawn with undefined strokeWidth, which is the usual value for inner pies) if (typeof strokeWidth === 'number') { circle = document.createElementNS(NS, "circle"); circle.setAttribute("cx", 0); circle.setAttribute("cy", 0); if (ringAlignRad > 0 && strokeWidth < ringWidth) { r = ringAlignRad; } else { r = rad - strokeWidth / 2; if (!overlap && ringAlign === self.RingAlign.INNER) { r -= ringWidth; } } circle.setAttribute("r", r); //Starting point of a circle's stroke is 3 o'clock by default. //Normally this point is invisible, but it might get visible if a stroke-dasharray is set //(which the user can do at any time via CSS): //Then this point where the stroke starts end ends is at 3 o'clock, but it should be at 12 o'clock, //since that's also the starting/ending point of the pie charts. Therefore, the circle will be //rotated 90 degrees anti-clockwise: circle.setAttribute("transform", "rotate(-90)"); if (strokeDashes) { setStrokeDashArray(circle, strokeDashes, 2.0 * Math.PI * r); } strokeColorConfigured = typeof strokeColor === 'string'; let stroke = strokeColorConfigured ? strokeColor : color; if (typeof stroke === "string") { circle.style.stroke = stroke; //In case of color animation this may be overwritten later on... } if (typeof strokeFill === "string") { circle.style.fill = strokeFill; } circle.style.strokeWidth = strokeWidth; circle.setAttribute("class", cssClassBackgroundCircle); addTitle(circle, title); svg.appendChild(circle); } const sw = ringWidth ? ringWidth : (overlap || typeof strokeWidth !== 'number') ? rad : rad - strokeWidth; if (ringAlignRad > 0 && ringWidth < strokeWidth) { //reduce ring radius for INNER or CENTER alignment with wider background circle's stroke //Note: ringAlignRad > 0 implies ringWidth and strokeWidth to be defined and > 0... r = ringAlignRad; } else { //ring radius max (except if to avoid overlap with outer background circle) r = rad - sw / 2; if (!overlap && typeof strokeWidth === 'number' && ringAlign === self.RingAlign.OUTER) { r -= strokeWidth; } } if (percent === 100 && !animationAttrs && typeof color === "string") { //Simply draw filled circle. (Not in CSS color mode, not with animation activated.) //"value" circle (full pie or ring) const circle2 = document.createElementNS(NS, "circle"); circle2.setAttribute("cx", 0); circle2.setAttribute("cy", 0); circle2.setAttribute("r", r); circle2.style.stroke = color; circle2.style.strokeWidth = sw; circle2.style.fill = "none"; circle2.setAttribute("class", cssClassForegroundPie); addTitle(circle2, title); svg.appendChild(circle2); } else if (percent > 0 && percent < 100 || (animationAttrs || typeof color === "undefined") && (percent === 0 || percent === 100)) { //2. Pie (or ring) const arc = document.createElementNS(NS, "path"); let arcToPercent = percent; //Before calculating the arc's path, first evaluate the optional animation. //Reason: For backwards animation, the arc has to span to the previous value instead // to the real target value, and only the delta part of it will be (animatedly) // made invisible via stroke dash properties. // I.e. the arcToPercent value may be overwritten in the following block: if (animationAttrs) { var delta = percent - prevPercent; var deltaArcLen = getArcLength(r, delta); var backwards = delta < 0; var animFrom; var animTo; if (backwards) { arcToPercent = prevPercent; animFrom = "0px"; animTo = -deltaArcLen + "px"; } else { animFrom = deltaArcLen + "px"; animTo = "0px"; } var arcLen = getArcLength(r, arcToPercent); arc.setAttribute("stroke-dasharray", arcLen + "px " + arcLen + "px"); arc.setAttribute("stroke-dashoffset", animFrom); //Setting the "static image" to the animFrom value, i.e. to the state of the image *before animation starts*, //a) requires the fill="freeze" attribute in order to finally (after animation) show the correct state (animTo). //b) ensures smooth animation without flicker (setting this attribute to animTo causes some browsers to display // the target state (animTo) for a slit second bevor animation starts, which can look irritating), //c) requires a SMIL detection fork (see smilSupported()): Since Browsers without SMIL support will display this static image and // never replace it with the animation's end state, setting this stroke-dashoffset attribute must only be // executed in browsers with SMIL support! // Setting this to animFrom would be compatible with no-SMIL-browsers, but for the price of said flicker. //=> This function (in this state) requires animationAttrs to be falsy if smilSupported() === false, see function call! addAnimationFromTo(arc, "stroke-dashoffset", "CSS", animFrom, animTo, animationAttrs); //Remove linecap when reduced to 0 percent! if (ringEndsRounded && percent === 0) { addAnimationFromTo(arc, "stroke-linecap", "CSS", "round", "butt", animationAttrs); } //Color Animation? if (prevColor && prevColor !== color) { addAnimationFromTo(arc, "stroke", "CSS", prevColor, color, animationAttrs); //Apply to outer circle's stroke? if (!strokeColorConfigured && circle) { circle.style.stroke = prevColor; addAnimationFromTo(circle, "stroke", "CSS", prevColor, color, animationAttrs); } } } const alpha = angle(arcToPercent); //Special case 100% (only in animated mode): targetX must not be 0: Arc won't be visible //if start and end point are identical. Move end point minimally to the left. //(Gap should not be visible if the graphic does not get scaled up too much.) const targetX = arcToPercent === 100 ? -0.00001 : Math.sin(alpha)*r; const targetY = Math.cos(alpha-Math.PI)*r; const largeArcFlag = arcToPercent > 50 ? "1" : "0"; const clockwiseFlag = "1"; const starty = -r; //start let path = "M0,"+starty; //arc path += " A"+r+","+r+" 0 "+largeArcFlag+","+clockwiseFlag+" "+targetX+","+targetY; arc.setAttribute("d", path); arc.style.fill = "none"; if (typeof color === "string") { arc.style.stroke = color; } arc.style.strokeWidth = sw; arc.style.strokeLinecap = ringEndsRounded && percent > 0 ? "round" : "butt"; if (rotation) { //rotation is "truthy". //May be "true" or a String (i.e. duration) or an object holding properties "duration" and "clockwise". const rotationStyleID="progresspie-rotation-style"; const rotationName="progresspie-rotate"; const rotationStyle="@keyframes "+ rotationName + " {100% {transform: rotate(360deg);}}"; if (!$("#" + rotationStyleID).length) { var head = $("head"); if (head.length) { const style = document.createElement("style"); style.id = rotationStyleID; $(style).text(rotationStyle); head.get(0).appendChild(style); } else { const style = document.createElementNS(NS, "style"); style.id = rotationStyleID; $(style).text(rotationStyle); defs.appendChild(style); } } const anticlockwise = rotation.clockwise === false; const dur = typeof rotation === "string" ? rotation : typeof rotation.duration === "string" ? rotation.duration : "1s"; //Default duration for true or any other truthy value is 1 second. // const anim = document.createElementNS(NS, "animateTransform"); // anim.setAttribute("attributeName", "transform"); // anim.setAttribute("attributeType", "XML"); // anim.setAttribute("type", "rotate"); // anim.setAttribute("from", "0"); // anim.setAttribute("to", anticlockwise ? "-360" : "360"); // anim.setAttribute("dur", dur); // anim.setAttribute("repeatDur", "indefinite"); // arc.appendChild(anim); const timing = typeof rotation.timing === "string" ? rotation.timing : "linear"; arc.style.animation = rotationName + " " + dur + " " + timing + (anticlockwise ? " reverse" : "") + " infinite"; } arc.setAttribute("class", cssClassForegroundPie); addTitle(arc, title); svg.appendChild(arc); } } function getRawValueStringOrNumber(me, opts) { var stringOrNumber; const vi = getValueInputObject(opts); if (vi !== null) { stringOrNumber = vi.val(); if (typeof opts.valueData !== "undefined" || typeof opts.valueAttr !== "undefined" || typeof opts.valueSelector !== "undefined") { throw new Error("options 'valueInput', 'valueData', 'valueAttr' and 'valueSelector' are mutually exclusive, i.e. at least three must be undefined!"); } } else if (typeof opts.valueData === "string") { stringOrNumber = me.data(opts.valueData); if (typeof opts.valueAttr !== "undefined" || typeof opts.valueSelector !== "undefined") { throw new Error("options 'valueData', 'valueAttr' and 'valueSelector' are mutually exclusive, i.e. at least two must be undefined!"); } } else if (typeof opts.valueData !== "undefined") { throw new Error("option 'valueData' is not of type 'string'!"); } else if (typeof opts.valueAttr === "string") { stringOrNumber = me.attr(opts.valueAttr); if (typeof opts.valueSelector !== "undefined") { throw new Error("options 'valueAttr' and 'valueSelector' are mutually exclusive, i.e. at least one must be undefined!"); } } else if (typeof opts.valueAttr !== "undefined") { throw new Error("option 'valueAttr' is not of type 'string'!"); } else if (typeof opts.valueSelector !== "undefined") { stringOrNumber = $(opts.valueSelector, me).text(); } if (typeof stringOrNumber === "undefined") { stringOrNumber = me.text(); } return stringOrNumber; } function getPercentValue(rawValueStringOrNumber, opts) { return Math.max(0, Math.min(100, opts.valueAdapter(rawValueStringOrNumber))); } function getModeAndColor(me, opts) { var mode = opts.mode; var color = opts.color; if (mode === self.Mode.CSS) { color = undefined; } else { //color may be a function or a constant var ct = typeof color; if (ct !== "undefined" && ct !== "string" && ct !== "function") { throw new Error("option 'color' has to be either a function or a string, but is of type '" + ct + "'!"); } if (ct === 'function') { mode = internalMode.USER_COLOR_FUNC; } else { if (ct === 'undefined' && typeof opts.colorAttr === "string") { color = me.attr(opts.colorAttr); } if (typeof color === 'string') { mode = internalMode.USER_COLOR_CONST; } else if (typeof opts.colorFunctionAttr === "string") { color = me.attr(opts.colorFunctionAttr); if (typeof color === 'string') { mode = internalMode.DATA_ATTR_FUNC; } } } } return {mode: mode, color: color}; } function calcColor(mode, userdefinedPieColor, percent) { return mode === internalMode.CSS ? undefined : mode === internalMode.MASK ? internalMode.MASK.color : mode === internalMode.IMASK ? internalMode.IMASK.color : mode === internalMode.GREY ? internalMode.GREY.color : mode === internalMode.GREEN ? self.colorByPercent(100) : mode === internalMode.RED ? self.colorByPercent(0) : mode === internalMode.COLOR || userdefinedPieColor === undefined ? self.colorByPercent(percent) : mode === internalMode.USER_COLOR_CONST ? userdefinedPieColor : mode === internalMode.USER_COLOR_FUNC ? userdefinedPieColor(percent) : mode === internalMode.DATA_ATTR_FUNC ? evalDataAttrFunc(userdefinedPieColor, percent) : "black"; } function calcFill(mode, opts, percent) { return mode === internalMode.CSS /*|| mode === internalMode.MASK */ ? undefined : typeof opts.backgroundColor === "string" ? opts.backgroundColor : typeof opts.backgroundColor === "function" ? opts.backgroundColor(percent) : mode === internalMode.IMASK ? internalMode.MASK.color : "none"; } function ctPluginIsFullSize(opts, pluginOpts) { return typeof opts.ringWidth === "undefined" || pluginOpts && pluginOpts.fullSize; } function drawRect(targetNode, rad, paddingProp, stroke, fill, strokeWidth) { var rect = document.createElementNS(NS, "rect"); targetNode.appendChild(rect); var left = rad + getMarginOrPaddingFromProp(paddingProp, 3); var top = rad + getMarginOrPaddingFromProp(paddingProp, 0); var width = left + rad + getMarginOrPaddingFromProp(paddingProp, 1); var height = top + rad + getMarginOrPaddingFromProp(paddingProp, 2); if (typeof strokeWidth === "number" && stroke !== "none") { rect.setAttribute("stroke-width", strokeWidth); width -= strokeWidth; height -= strokeWidth; left -= strokeWidth / 2; top -= strokeWidth / 2; } rect.setAttribute("x", "-" + left); rect.setAttribute("y", "-" + top); rect.setAttribute("width", width); rect.setAttribute("height", height); rect.setAttribute("stroke", stroke); rect.setAttribute("fill", fill); } function createSVG(rad, opts) { var svg = document.createElementNS(NS, "svg"); var leftWidth = rad + opts.getPadding(3) + opts.getMargin(3); var topHeight = rad + opts.getPadding(0) + opts.getMargin(0); var totalWidth = leftWidth + rad + opts.getPadding(1) + opts.getMargin(1); var totalHeight = topHeight + rad + opts.getPadding(2) + opts.getMargin(2); var scaledWidth = totalWidth; var scaledHeight = totalHeight; if (typeof opts.scale === "number") { scaledWidth *= opts.scale; scaledHeight *= opts.scale; } svg.setAttribute("width", Math.ceil(scaledWidth)); svg.setAttribute("height", Math.ceil(scaledHeight)); svg.setAttribute("viewBox", "-" + leftWidth + " -" + topHeight + " " + totalWidth + " " + totalHeight); return svg; } /** * returns raw and percent value (as well as previous value and more) * and updates options by application of optionsByRawValue() or optionsByPercent() * functions present in options. * I.e. the options object may be extended. * Returns object with values. */ function getValuesAndUpdateOpts(me, opts, innerLevel) { var out = {}; out.raw = getRawValueStringOrNumber(me, opts); if (typeof opts.optionsByRawValue === "function") { var newOptsRaw = opts.optionsByRawValue(out.raw); if (typeof newOptsRaw !== "undefined" && newOptsRaw !== null) { $.extend(opts, newOptsRaw); //Update raw in case newOptsRaw defines new value data selector out.raw = getRawValueStringOrNumber(me, opts); } } out.p = getPercentValue(out.raw, opts); var prevDataName = innerLevel === 0 ? self.prevValueDataName : self.prevInnerValueDataName; if (innerLevel > 1) { prevDataName += innerLevel; } out.prevP = me.data(prevDataName); out.isInitialValue = typeof out.prevP === 'undefined'; me.data(prevDataName, out.p); if (typeof out.prevP !== 'number') { out.prevP = 0; } if (typeof opts.optionsByPercent === "function") { var newOpts = opts.optionsByPercent(out.p); if (typeof newOpts !== "undefined" && newOpts !== null) { $.extend(opts, newOpts); //Update values in case the optionsByPercent define different value adapter functions or value data selectors out.raw = getRawValueStringOrNumber(me, opts); out.p = getPercentValue(out.raw, opts); } } return out; } $(this).each(function () { const me = $(this); let opts = $.extend({}, globalOpts); if (noargs) { var localOpts = $(this).data($.fn.setupProgressPie.dataKey); if (typeof localOpts === "object") { opts = $.extend({}, localOpts, optsMethods); //use stored individual setup instead of gobalOpts (which in this case (noargs) are just defaults anyway). } } var existing = $("svg", me); //existing SVGs in target element if (!existing.length || opts.update) { //Only draw if no SVG already existing or update mode if (existing.length && opts.update) { //remove existing SVG existing.remove(); opts.separator = ''; //reset any separator when applying an update in order not to repeatedly insert a new one with each update. } var values = getValuesAndUpdateOpts(me, opts, 0); var h = Math.ceil(typeof opts.size === "number" ? opts.size : me.height()); if (h === 0) { h = 20; } h *= opts.sizeFactor; var rad = h / 2; var totalRad = rad; var mc = getModeAndColor(me, opts); //Note: mc.mode is an internal mode which gets replaced by internal mode constants in case //user defined colors or color functions should be applied. The function calcColor/calcFill //should be called with mc.mode. //But since in combinations of MASK or IMASK mode with user-defined color functions, //mc.mode will take an internal mode reflecting how to calculate the color and loose the //information of the mask mode, any check not calculating the color but the draw mode //(i.e. decide whether to draw a mask or a visible diagram element) has to be based //on the original opts.mode instead of mc.mode! var color = calcColor(mc.mode, mc.color, values.p); var fill = calcFill(mc.mode, opts, values.p); var prevColor; if (opts.animateColor === true || typeof opts.animateColor === "undefined" && !values.isInitialValue) { prevColor = calcColor(mc.mode, mc.color, values.prevP); } var animationAttrs = !self.smilSupported() ? null : opts.animate === true ? self.defaultAnimationAttributes : typeof opts.animate === 'object' ? $.extend({}, self.defaultAnimationAttributes, opts.animate) : null; //Check for content plug-in and whether the pie chart is to be drawn at all: var ctPlugins = null; var hideChart = false; var ctPluginBaseCheckArgs = { isFullSize: function() { return ctPluginIsFullSize(opts, this); }, isCssMode: function() { return typeof this.color !== "string"; }, color: color, percentValue: values.p, rawValue: values.raw, pieOpts: opts }; if (opts.contentPlugin) { ctPlugins = getContentPlugins(opts.contentPlugin); for (var pluginIndex = 0; pluginIndex < ctPlugins.length; pluginIndex++) { var ctPlugin = ctPlugins[pluginIndex]; var ctpOpts = getContentPluginOptions(opts.contentPluginOptions, pluginIndex); var checkArgs = ctPluginBaseCheckArgs; if (ctpOpts !== null && typeof ctpOpts === "object") { checkArgs = $.extend({}, ctPluginBaseCheckArgs, ctpOpts); } if (typeof ctPlugin === 'object' && typeof ctPlugin.hidesChartIfFullSize === 'function') { hideChart = hideChart || (opts.mode !== self.Mode.MASK && //in MASK mode the chart is a mask and cannot be hidden by content! opts.mode !== self.Mode.IMASK && ctPluginIsFullSize(opts, ctpOpts) && ctPlugin.hidesChartIfFullSize(checkArgs)); } } } //Create and insert SVG... var svg = createSVG(rad, opts); var defs = document.createElementNS(NS, "defs"); if (mc.mode !== self.Mode.CSS) { svg.style.verticalAlign = opts.verticalAlign; } if (me.is(":empty")) { //simply insert (regardless of prepend option, and without separator) me.append(svg); } else if (opts.prepend) { me.prepend(svg, opts.separator); } else { me.append(opts.separator, svg); } //Optionally add title to SVG: addTitle(svg, opts.globalTitle); //Draw/insert Pie var maskId = null; var chartTargetNode = svg; if (!hideChart) { if (opts.mode === self.Mode.MASK || opts.mode === self.Mode.IMASK) { chartTargetNode = document.createElementNS(NS, "mask"); defs.appendChild(chartTargetNode); maskId = createId("pie"); chartTargetNode.setAttribute("id", maskId); if (opts.mode === self.Mode.IMASK) { //fill the background behind the black pie with a white rectangle to complete the inverted mask: drawRect(chartTargetNode, rad, opts.padding, "none", fill); } } var cssForeground = opts.cssClassForegroundPie; var cssBackground = opts.cssClassBackgroundCircle; if (typeof opts.inner === 'object') { cssForeground += " " + opts.cssClassOuter; cssBackground += " " + opts.cssClassOuter; } drawPie(chartTargetNode, defs, rad, opts.strokeWidth, opts.strokeColor, opts.strokeDashes, fill, opts.overlap, opts.ringWidth, opts.ringEndsRounded, opts.ringAlign, cssBackground, cssForeground, values.p, values.prevP, color, prevColor, opts.title, animationAttrs, opts.rotation); } //w: ringWidth of innermost ring to calculate free disc inside avaliable for content plug-in. var w = typeof opts.ringWidth === 'number' ? opts.ringWidth : typeof opts.strokeWidth === 'number' ? opts.strokeWidth : 0; //Draw a second, inner pie? var inner = opts.inner; var innerCnt = 0; while (typeof inner === 'object') { innerCnt++; inner = $.extend({}, inner); //make copy before modifications if (typeof inner.valueAdapter === "undefined") { inner.valueAdapter = self.defaults.valueAdapter; } if (typeof inner.overlap === 'undefined') { inner.overlap = self.defaults.overlap; } if (typeof inner.ringAlign === 'undefined') { //inherit from outer inner.ringAlign = opts.ringAlign; //(must not be undefined) } var innerValues = getValuesAndUpdateOpts(me, inner, innerCnt); var cssClassName = opts.cssClassInner; if (innerCnt > 1) { cssClassName += innerCnt; } mc = getModeAndColor(me, inner); rad = typeof inner.size === "number" ? inner.size * opts.sizeFactor / 2 : rad * 0.6; var innerColor = calcColor(mc.mode, mc.color, innerValues.p); var innerPrevColor = null; if (inner.animateColor === true || typeof inner.animateColor === "undefined" && (opts.animateColor === true || typeof opts.animateColor === "undefined" && innerValues.isInitialValue)) { innerPrevColor = calcColor(mc.mode, mc.color, innerValues.prevP); } if (inner.animate === false || !self.smilSupported()) { animationAttrs = null; } else if (inner.animate === true && animationAttrs === null) { animationAttrs = self.defaultAnimationAttributes; } else if (typeof inner.animate === "object") { if (animationAttrs === null) { animationAttrs = $.extend({}, self.defaultAnimationAttributes, inner.animate); } else { animationAttrs = $.extend({}, animationAttrs, inner.animate); } } if (!hideChart) { drawPie(chartTargetNode, defs, rad, inner.strokeWidth, inner.strokeColor, inner.strokeDashes, fill, inner.overlap, inner.ringWidth, inner.ringEndsRounded, inner.ringAlign, opts.cssClassBackgroundCircle + " " + cssClassName, opts.cssClassForegroundPie + " " + cssClassName, innerValues.p, innerValues.prevP, innerColor, innerPrevColor, inner.title, animationAttrs, inner.rotation); } w = typeof inner.ringWidth === 'number' ? inner.ringWidth : 0; inner = inner.inner; } if (ctPlugins !== null) { var r = rad; if (w < rad) { r -= w; } var baseArgs = $.extend({ newSvgElement: function(name) { var el = document.createElementNS(NS, name); group.appendChild(el); return el; }, newSvgSubelement: function(parent, name) { var el = document.createElementNS(NS, name); parent.appendChild(el); return el; }, newDefElement: function(name) { var el = document.createElementNS(NS, name); defs.appendChild(el); return el; }, createId: createId, getBackgroundRadius: function(ignoreMargin) { var r = this.isFullSize() ? this.totalRadius: this.radius; if (! ignoreMargin) { var margin = typeof this.margin === "number" ? this.margin : this.isFullSize() ? this.pieOpts.defaultContentPluginBackgroundMarginFullSize : this.pieOpts.defaultContentPluginBackgroundMarginInsideRing; r -= margin; } return r; }, addBackground: function(radius, cssClassName) { //fill background if set const classNameSet = typeof cssClassName === "string"; if (this.backgroundColor || classNameSet) { var bg = this.newSvgElement("circle"); bg.setAttribute("cx", "0"); bg.setAttribute("cy", "0"); bg.setAttribute("r", radius); if (this.backgroundColor) { bg.setAttribute("fill", this.backgroundColor); } if (classNameSet) { bg.setAttribute("class", cssClassName); } } }, addBackgroundRect: function(stroke, fill, strokeWidth) { drawRect(group, totalRad, opts.padding, stroke, fill, strokeWidth); }, getContentPlugin: getContentPlugin, radius: r, totalRadius: totalRad, color: color, percentValue: values.p, rawValue: values.raw }, ctPluginBaseCheckArgs); var maskNotAppliedYet = true; for (var pluginIndex2 = 0; pluginIndex2 < ctPlugins.length; pluginIndex2++) { var ctPlugin2 = ctPlugins[pluginIndex2]; var group = document.createElementNS(NS, "g"); var f = typeof ctPlugin2 === 'function' ? ctPlugin2 : ctPlugin2.draw; var args = baseArgs; var ctpOpts2 = getContentPluginOptions(opts.contentPluginOptions, pluginIndex2); if (ctpOpts2 !== null && typeof ctpOpts2 === 'object') { args = $.extend({}, baseArgs, ctpOpts2); } f(args); if (typeof ctPlugin2.inBackground === 'boolean' && ctPlugin2.inBackground || typeof ctPlugin2.inBackground === 'function' && ctPlugin2.inBackground(args) ) { $(svg).prepend(group); if (maskId !== null && maskNotAppliedYet) { group.setAttribute("mask", "url(#" + maskId + ")"); maskNotAppliedYet = false; } } else { $(svg).append(group); } } if (maskId !== null && maskNotAppliedYet) { throw new Error("MASK mode could not be applied since no content plug-in drew a background to be masked! " + "You need do specify at least one content plug-in which draws into the background!"); } } if (defs.hasChildNodes()){ $(svg).prepend(defs); } } }); return this; }; /** * Enum defining possible values for the mode option. * @memberOf jQuery.fn.progressPie * @enum * @readonly */ $.fn.progressPie.Mode = { /** Default Mode: Pie is drawn in a shade of grey. The HTML color code is "#888" and may be changed by * overwriting jQuery.fn.progressPie.Mode.GREY.color (of type string). * @type {Object} */ GREY:{color:"#888"}, /** In mode RED the pie is drawn in red color regardless of the percentual value. * jQuery.fn.progressPie.Mode.RED.value is a variable of type "number" with the default value * of 200 and means the red color will be rgb(200, 0, 0). * The variable RED.value is not only used in mode RED, but also in mode COLOR for calculating the * color of any value between 0 and 50. * @type {Object} */ RED:{value:200}, /** In mode GREEN the pie is drawn in green color regardless of the percentual value. * jQuery.fn.progressPie.Mode.GREEN.value is a variable of type "number" with the default value * of 200 and means the green color will be rgb(0, 200, 0). * The variable GREEN.value is not only used in mode GREE, but also in mode COLOR for calculating the * color of any value between 50 and 100. * @type {Object} */ GREEN:{value:200}, /** In mode COLOR the color of the pie is depending on the percentual value. * The color is calculated via $.fn.progressPie".colorByPercent. * It's the same green color as in mode GREEN for a value of 100 percent, the same red color * as in mode RED for a value of 0%. * The colors may be altered by overwriting progressPie.Mode.RED.value or progressPie.Mode.GREEN.value. * @type {Object} */ COLOR:{}, /** In mode CSS the color style properties {@code stroke} and {@code fill} of the background circle * and the {@code stroke} property of the foreground (pie or ring) are not set at all and are * required to be set via CSS rules by the user. (The {@code fill} style of the foreground * is always set to 'none', even in CSS mode.) */ CSS:{}, /** * Mask Mode: Requires the use of a content plug-in which adds background content, otherwise * your chart will stay completely invisible! * If a chart calls one or more content plug-ins which add background content (the pie will be * used as a mask applied to the topmost background layer (i.e. the output of the first background * content plug-in). In the mask, the areas covered by the pie will be drawn in color * jQuery.fn.progressPie.Mode.MASK.color which defaults to 'white', while all the * areas not covered by the chart stay transparent. Effectively this default means that only those * parts of the background layer that would normally be covered by the chart will stay visible * while the rest of the background image will be clipped away. *

MASK mode may be combined with options like color, strokeColor * and backgroundColor in order to change the mask's transparency for the pie, * it's background circle and the filling of the latter.

*

(See examples page for content plug-ins * demonstrating the MASK mode with background images.) * @type {Object} */ MASK:{color: "white"}, /** * Inverted Mask Mode: Very similar to MASK mode, only the mask is inverted: By default, * the image areas covered by the pie will not show the background but all the rest of the image, * the chart will be "cut out of" the background layer. *

This is achieved by drawing the pie in a mask layer with color * jQuery.fm.progressPie.Mode.IMASK.color, which defaults to 'black' * on a background rectangle filled in color * jQuery.fm.progressPie.Mode.MASK.color, which defaults to 'white'. *

IMASK mode may be combined with options like color, strokeColor * and backgroundColor, with color overriding the pie's mask color * (defaulting to black) and background overriding the pie's fill color (defaulting * to white) and the filling of the background rectangle. * The latter makes the difference in comparison to just using MASK mode and switching * the color and backgroundColor value: In MASK mode, * backgroundColor only defines a fill color for the inside of the circular chart, * in IMASK mode the whole rectangular chart area (including the padding, if set) will be filled! *

*/ IMASK:{color: "black"} }; /** * public static method to calculate a color for a percent value: green for 100%, red for 0%, yellow for 50%, * gradients for values in between. * This is used internally in mode progressPie.Mode.COLOR * @param {number} percent - a value between 0 and 100 (inclusive). 0 results in red color, 100 in green, 50 in yellow, * any other value greater than 50 generates a gradient between green and yellow, values less than 50 a gradient * between red and yellow. * @param {number} alpha - optional second parameter. If defined, the calculated color will not be a fully opaque * rgb(r,g,b) value, but instead rgba(r,g,b,alpha). Has to be a number between 0 and 1 inclusive * and defines the transparency (0 for fully transparent, 1 for fully opaque). * @memberOf jQuery.fn.progressPie * @function colorByPercent */ $.fn.progressPie.colorByPercent = function(percent, alpha) { var maxGreen = $.fn.progressPie.Mode.GREEN.value; var maxRed = $.fn.progressPie.Mode.RED.value; var green = percent > 50 ? maxGreen : Math.floor(maxGreen * percent / 50); var red = percent < 50 ? maxRed : Math.floor(maxRed * (100 - percent) / 50); var rgb = red + "," + green + ",0"; return typeof alpha === "number" ? "rgba(" + rgb + "," + alpha +")" : "rgb(" + rgb + ")"; }; $.fn.progressPie.smilSupported = function() { if (typeof $.fn.progressPie.smilSupported.cache === "undefined") { //Test taken from Modernizr Library (MIT License) with special thanks to that project. //This one line is pretty much identical to Modernizr's SMIL test routine, but by extracting it from that library, //I don't need the whole Modernizr Framework around that test. This one line is actually more compact than even the //smallest Modernizr custom feature build (supporting only the SMIL test and not generating CSS classes), //and by integrating it here, I don't introduce unnecessary library dependencies. $.fn.progressPie.smilSupported.cache = /SVGAnimate/.test(document.createElementNS("http://www.w3.org/2000/svg", "animate").toString()); } return $.fn.progressPie.smilSupported.cache; }; /** * Enum defining possible valus for the ringAlign option. * @memberOf jQuery.fn.progressPie * @enum * @readonly */ $.fn.progressPie.RingAlign = { /** * Both strokes (background circle and ring graph) are drawn on the * outer edge of the (circular) chart.
* If the overlap option is set to false, the outer edge of the ring * will be aligned with the inner edge of the background circle, i.e. the ring * will be drawn inside of the background circle. */ OUTER: {}, /** * In this mode, the ring is drawn centered on top of background circle * (provided, the overlap option is not turned off). If the ring * is wider, it will overlap the background circle equally to the outside and to the * inside of the circle, if it is slimmer, it will be centered "inside" the background * circle. */ CENTER: {}, /** * In this mode, both of the two stroke (background circle and ring graph) will align * towards the center of the circle, i.e. share the same inner radius.
* If the overlap option is set to false, the inner edge of the ring * will be aligned with the outer edge of the background circle, i.e. the ring will * be drawn around the background circle, the latter will be shrunk to fit into * the ring. */ INNER: {} }; /** * Default Options. * This is a public (static) object in order to allow users to globally modify the defaults * before using the plug-in. * @memberOf jQuery.fn.progressPie * @member defaults * @property {Mode} mode - A value of the enum type Mode, defaults to $.fn.progressPie.Mode.GREY * @property {number} margin - a single number or an array of up to four numbers, defaults to 0 (no margin at all). * If this is a single number (or an array of length 1), this is the width of a region around the pie chart to be left blank (transparent), * i.e. number 5 means that the image is going to be 10 pixels wider and higher because a 5 pixel wide margin is inserted all around the pie. * If this is an array of length 4, the first value is the top margin, the second the right margin, the third is the bottom margin and the * fourth is the left margin, i.e. the margins are specified "clockwise" starting at 12 o'clock, just like in CSS's shorthand margin syntax. * If this option is an array of length 3, only the top, right and bottom margin are specified directly and the left margin will be the same * as the right margin. If this is an array of length 2, the first value specifies top and bottom margin and the second right and left margin. * @property {number} padding - a single number or an array of up to four numbers, defaults to 0 (no padding at all). * A padding is an inner border left free between the margin and the pie chart. In most cases, margin and padding behave the same. If * both are specified, they add up. A difference may only be seen when using certain content plug-ins (like the predefined image plug-in), * which may for example add a background image or color not only covering the area of the pie chart itself, but the chart plus its padding * (but not covering the margin). * @property {number} strokeWidth - The default width of the background circle stroke, defaults to 2 * @property {boolean} overlap - if true (default), the foreground pie or ring fragment is drawn full size * on top of the always visible background circle stroke, overlapping it. If set to false, the foreground pie/ring * will be scaled down to fit into the background circle, not overlapping the latter's stroke. * This is only advisable if the strokeWidth is small enough to leave free space inside the * background circle. Also, this only makes any sense if the background circle's color differs from the * foreground color (i.e. the strokeColor option is set) or if the foreground color is semi transparent. * @property {RingAlign} ringAlign: defaults to $.fn.progressPie.RingAlign.OUTER, defines the alignment of a ring chart (only defined * if the ringWidth option is set and > 0) with the background circle (requires the strokeWidth option to be > 0). * @property {boolean} prepend - true for prepending the SVG graph to the selected element's content, false for appending. Defaults to true. * @property {string} separator - String to be inserted between prepended or appended SVG and target element's content. * If the target element is empty, i.e. there's no content to append or prepend the graph to * (example: <span class="pie" data-percent="10"></span>), the separator and prepend options will be ignored * and only the SVG will be inserted into the element (starting with V2.0.0) * @property {string} verticalAlign - CSS value for the verticalAlign style attribute of the inserted SVG node (defaults to "bottom", option is ignored in special CSS mode). * @property {boolean} update - true will remove any SVG child from the selected target element before inserting a new image, * false will only insert a new SVG if none exists yet. Defaults to false. * @property {function} valueAdapter - Function takes a value (string or number) and returns a number in range (0..100), * defaults to a function returning number values unchanged and applying parseFloat to string values. Note that this may * parse percent numbers with decimal digits if the "dot" is used as decimal separator, while if any unknown character * like a comma (european decimal separator) is found, the parsing stops and the rest of the string is ignored. * I.e. a string like "23.5" is parsed as twenty-three and a half percent while "23,5" is parsed as exactly * 23 percent, which usually should be exact enough if the pie chart is not very big. * @property {string} valueInputEvents - space-separated list of event names, defaulting to "change". * This property is only used in conjunction with the valueInput option (and only using the * setupProgressPie() function). The valueInput option binds a progress pie/ring * to an input element and automatically registers an event handler listening on the input element to these events. * If any of these events (by default only the change event) fires, the pie will automatically be * updated to the current input's value (.val()). * @property {boolean} ringEndsRounded - If setting a ringWidth, this flag controls if the ends of the ring are simply * cut (false) or if half a circle is appended to each end of the ring section (true). Defaults to false. * @property {number} sizeFactor - Defaults to 1. The "original" diameter for the pie chart as either auto-sized * or specified by the size option, is multiplied with this factor to get the final diameter before * drawing the pie chart. * @property {number} scale - Defaults to 1. The already rendered SVG graphic is finally scaled by this factor. * In difference to sizeFactor this does not simply change the diameter/radius of the chart, but scales * all other aspects such as the strokeWidth, too. * @property {number} defaultContentPluginBackgroundMarginFullSize - Defaults to 0. Sets the default value for a content plug-in's margin * property if that plug-in uses the API's getBackgroundRadius() function, if the contentPluginOptions object does not * specify a margin property and if a pie chart is drawn (i.e. the ringWidth option is not set) or if (on a ring chart) * the fullSize property of the contentPluginOption is set to true.
* The value of 0 causes a filled background to cover the whole pie. * @property {number} defaultContentPluginBackgroundMarginInsideRing - Defaults to 1. Sets the default value for a content plug-in's margin * property if that plug-in uses the API's getBackgroundRadius() function, if the contentPluginOptions object does not * specify a margin property and does not set fullSize and if a ring is drawn (i.e. the ringWidth option is set).
* The default value of 1 leaves free circular gap of 1 pixel between the ring and the filled content plug-in's background inside the ring. With a value of zero, * the content background would "touch" the ring. * @property {string} cssClassBackgroundCircle - name of a CSS class assigned to the circle shape drawn as background * behind the pie or ring segment. Defaults to progresspie-background. * @property {string} cssClassForegroundPie - name of a CSS class assigned to the pie or ring segment (foreground). * Defaults to progresspie-foreground. * @property {string} cssClassOuter - If the inner option is used to draw a second value, this CSS * class is assigned to the background circle as well as the foreground pie/ring segment of the outer/main value * in addition to the respective cssClassBackgroundCircle or cssClassForegroundPie class. * Defaults to progresspie-outer. * @property {string} cssClassOuter - If the inner option is used to draw a second value, this CSS * class as assigned to the background circle and the forground pie/ring segment of the inner value * in addition to the respective cssClassBackgroundCircle or cssClassForegroundPie. * Defaults to progresspie-inner. If the inner option contains a second inner option (third value), * the background and foreground elements of the "inner inner" value get assigned this class with suffix "2", * an "inner inner inner" value will be assigned this class with suffix "3" and so on. */ $.fn.progressPie.defaults = { mode: $.fn.progressPie.Mode.GREY, margin: 0, padding: 0, strokeWidth: 2, overlap: true, ringAlign: $.fn.progressPie.RingAlign.OUTER, prepend: true, separator: " ", verticalAlign: "bottom", update: false, valueAdapter: function(value) { if (typeof value === "string") { return parseFloat(value); } else if (typeof value === "number") { return value; } else { return 0; } }, valueInputEvents: "change", ringEndsRounded: false, sizeFactor: 1, scale: 1, defaultContentPluginBackgroundMarginFullSize: 0, defaultContentPluginBackgroundMarginInsideRing: 1, cssClassBackgroundCircle: "progresspie-background", cssClassForegroundPie: "progresspie-foreground", cssClassOuter: "progresspie-outer", cssClassInner: "progresspie-inner" }; /** * Default SMIL animation attributes for value transitions. * keys and value syntax follow the SMIL language for SVG animation. * Each property of this object will be turned into an attribute of the SMIL animation * element, the property's key serving as attribute name, the property's value as attribute value. *

If the plug-in is called with option animate: true, these options will be applied to * the animation.
* If the plug-in is called with option animate: {options}, the options enumerated by the * user get added to these defaults. Each stated option will override the default option, while those * properties of this defaults object that are not overridden by the user will be applied unchanged.
* If the user, for example, adds the option animate: {dur: "2s"}, the default duration will * be overriden, the animation will last 2 seconds, while the other animation properties (spline mode) * will be applied exactly as defined in this defaults object. * @memberOf jQuery.fn.progressPie * @member defaultAnimationAttributes * @property {string} dur - the duration of the animation (number with unit, e.g. "1s" or "700ms") * @property {string} calcMode - mode for calculating the animation speed, defaults to "spline", see SMIL {@link http://www.w3.org/TR/SVG/animate.html#CalcModeAttribute specification}. * @property {string} keySplines - see {@link http://www.w3.org/TR/SVG/animate.html#KeySplinesAttribute specification} * @property {string} keyTimes - see {@link http://www.w3.org/TR/SVG/animate.html#KeyTimesAttribute specification} */ $.fn.progressPie.defaultAnimationAttributes = { dur: "1s", calcMode: "spline", keySplines: "0.23 1 0.32 1", keyTimes: "0;1" }; /** * Default namespace for content plug-ins. * If you write contentPlugin functions, it is recommended to add them as methods to this object * (see bundled jquery-progresspiesvg-controlIcons.js for eample). * Though you may use any function as a plugin (if it conforms to the plug-in interface), * only functions within this default namespace may be specified by a string holding their function name * in the contentPlugin option. Functions not in this namespace have to be referred to * by a function reference (an expression evaluating to the very function object). *

* Any methods of this object documented in the bundled JSDoc are predefined content plug-ins, and member * objects are used to define default options for those plug-in methods. * @namespace contentPlugin * @memberOf jQuery.fn.progressPie */ $.fn.progressPie.contentPlugin = {}; /** * If a user enables animated state transitions via the animate option, the plug-in * stores the last drawn value in jQuery's data map associated with the target element. If the user * later first updates the value to be shown and then calls the progressPie() plug-in again on the target * element, it will not only find the new value, but also the previous value in said data, which it * needs to calculate the transition. *

This option defines the name of the data entry which will be added to the target DOM node * to hold the lastly draw percent value. * @memberOf jQuery.fn.progressPie * @member prevValueDataName */ $.fn.progressPie.prevValueDataName = "_progresspieSVG_prevValue"; /** * Just like prevValueDataName, but used for double/multiple pies: * The lastly drawn percent value for the inner pie will be stored in a data * entry of this name. * If even more than two values are drawn (by nested inner options), the value of * a number will be appended to this name. So the lastly drawn value of the third pie (inner.inner) * will be stored in a data entry named prevInnerValueDataName+"2". * @memberOf jQuery.fn.progressPie * @member prevInnerValueDataName */ $.fn.progressPie.prevInnerValueDataName = "_progresspieSVG_prevInnerValue"; }( jQuery ));