Licitator 1.0
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1341 lines
64 KiB

5 years ago
  1. /**
  2. * @license
  3. * Copyright (c) 2018, Immo Schulz-Gerlach, www.isg-software.de
  4. * All rights reserved.
  5. *
  6. * Redistribution and use in source and binary forms, with or without modification, are
  7. * permitted provided that the following conditions are met:
  8. *
  9. * 1. Redistributions of source code must retain the above copyright notice, this list of
  10. * conditions and the following disclaimer.
  11. *
  12. * 2. Redistributions in binary form must reproduce the above copyright notice, this list
  13. * of conditions and the following disclaimer in the documentation and/or other materials
  14. * provided with the distribution.
  15. *
  16. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
  17. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  18. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
  19. * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  20. * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
  21. * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
  22. * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  23. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
  24. * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  25. */
  26. /**
  27. * Namespace of jQuery. Usually bound to the alias <code>$</code>.
  28. *
  29. * @see http://jquery.com/
  30. * @namespace jQuery
  31. */
  32. /**
  33. * Namespace for jQuery plug-ins.
  34. *
  35. * @see http://jquery.com/
  36. * @namespace fn
  37. * @memberOf jQuery
  38. */
  39. (function( $ ) {
  40. "use strict";
  41. /**
  42. * Namespace for this jQuery plugin
  43. * @namespace progressPie
  44. * @memberOf jQuery.fn
  45. */
  46. var idCounter = {};
  47. function getValueInputObject(options) {
  48. let o = options.valueInput;
  49. if (typeof o === "object") {
  50. if (typeof o.val === "function") {
  51. return o;
  52. } else {
  53. 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.");
  54. }
  55. } else if (typeof o === "string") {
  56. return $(o);
  57. } else {
  58. return null;
  59. }
  60. }
  61. /**
  62. * Stores options for the progressPie plug-in. If this plug-in function is called, any succeeding calls to the progressPie plug-in
  63. * without argument will behave the same like when called with the options stored here.
  64. * This is recommended, if the progressPie plug-in gets called repeatedly (to update its graphic due to changed values).
  65. * Then, this setup provides the means to set the options only once and to keep update calls simple instead of
  66. * calling the progressPie repeatedly with the same options argument over and over again.
  67. * <p>The "update" option, if not specified in the options of this call, will default to true (regardless of
  68. * the value defined in $.fn.progressPie.defaults.</p>
  69. * <p>Usage pattern:</p>
  70. * <pre><code>$(selector).setupProgressPie({options}).progressPie();
  71. * update value;
  72. * $(selector).progresssPie(); //update the graphic using the same options.
  73. * </code></pre>
  74. * <p>Repeated calls of setupProgressPie are allowed and will update the options: The options of the subsequent call
  75. * get merged into the existing setup. Example:
  76. * <pre><code>
  77. * $(selector).setupProgressPie({color: "green", strokeWidth: 3});
  78. * ...
  79. * $(selector).setupProgressPie({color: "navy"});
  80. * </code></pre>
  81. * <p>In this example the second call will change the color for any following call of <code>progressPie()</code>, but
  82. * will leave the <code>strokeWidth: 3</code> option untouched, i.e. will not reset it to the default.</p>
  83. * <p>Exception of this rule: You may add a second argument to the plugin call of type boolean. If you append
  84. * <code>, true</code>, the options will not be merged into an existing setup, but will completely overwrite
  85. * any existing setup like this was the first call of the setup method at all.</p>
  86. * <p>Suppose, in the example above, we change the second setup call to:</p>
  87. * <pre><code>$(selector.setupProgressPie({color: "navy"}, true);</code></pre>
  88. * <p>Then, this call will not only change the color from green to navy, but also reset the strokeWidth to default.</p>
  89. * @function setupProgressPie()
  90. * @memberOf jQuery.fn
  91. * @param {object} options - object containing individual options (merged with default options)
  92. * @param {boolean} replace - if true, the any previous setup will be completely replaced by this new setup.
  93. * Any property not configured in the options object will be reset to its default value (which may be either be undefined
  94. * or defined by the jQuery.fn.progressPie.defaults object). If false (or falsy, including null or undefined, i.e.
  95. * this also applies if you don't state an actual second argument at all), the passed options will be merged
  96. * into any already existing setup.
  97. * @return this / result set (for chainable method calls on the result set)
  98. */
  99. $.fn.setupProgressPie = function(options, replace) {
  100. $(this).each(function() {
  101. var existingSetup = $(this).data($.fn.setupProgressPie.dataKey);
  102. if (replace || typeof existingSetup !== "object") {
  103. var opts = $.extend( {}, $.fn.progressPie.defaults, {update: true}, options );
  104. $(this).data($.fn.setupProgressPie.dataKey, opts);
  105. } else {
  106. $.extend(existingSetup, options);
  107. }
  108. });
  109. const opts = $.extend({}, $.fn.progressPie.defaults, options);
  110. const vi = getValueInputObject(opts);
  111. if (vi !== null) {
  112. if (typeof opts.valueInputEvents !== "string") {
  113. throw new Error("'valueInputEvents' has to be a string (space-separated list of event names)!");
  114. }
  115. vi.on(opts.valueInputEvents, () => {
  116. $(this).progressPie();
  117. });
  118. }
  119. return this;
  120. };
  121. $.fn.setupProgressPie.dataKey = "$.fn.setupProgressPie";
  122. /**
  123. * This plug-in may be used to draw a piechart with only one filled pie (rest empty).
  124. * It is designed to be an alternative to a progress bar, since it simply depicts a single value (in percent).
  125. * The plug-in assumes that values in percent are part of the document (either visible or as attribute value)
  126. * and a small pie representing each value is to be dynamically inserted. This may be either a static display
  127. * or the pie may be updated upon data changes.
  128. *
  129. * <p>Typical application: Append or prepend to visible percent value:<br>
  130. * This mode assumes by default, that a value (integer number between 0 and 100 inclusive,
  131. * floating point numbers are supported, but truncated) is the only text content of an HTML element, e.g. a span
  132. * element, and the pie is to be prepended (or appended) to the same element. The pie will usually be auto-sized to fit
  133. * into the text line. A separator String to be placed between the pie and the number may be configured.
  134. * Defaults are to prepend the pie and use a non-breaking space as separator.
  135. * E.g. say you have HTML code like <code>&lt;p&gt;You have achieved 25 points out of 50 (&lt;span class="pie"&gt;50&lt;/span&gt;%)&lt;/p&gt;</code>,
  136. * then you may insert a pie filled by 50% with <code>$(.pie).progressPie();</code>, resulting in a line like:
  137. * <code>&lt;p&gt;You have achieved 25 points out of 50 (&lt;span class="pie"&gt;&lt;svg&gt;the pie chart&lt;/svg&gt;&amp;nbsp;50&lt;/span&gt;%)&lt;/p&gt;</code>
  138. *
  139. * <p>Usage:
  140. * Select the elements holding the percent number and to insert the pie into by a jQuery selector.
  141. * On the jQuery result set call "progressPie(options)", where options is an optional object
  142. * with configuration options. See <a href="index.html">Home</a> (or README) for a documentation of supported options.
  143. * The plugin is applied to any element in the result set, i.e. if the selector did not found any matching
  144. * element, nothing will happen, while if the selector found several matching elements, the plugin
  145. * will try to insert a corresponding pie into each of the found elements individually.
  146. *
  147. * <p>The progressPie method will <code>return this</code>, enabling chaining of method calls
  148. * on the result set.</p>
  149. *
  150. * @function progressPie()
  151. * @memberOf jQuery.fn
  152. * @param options - object containing individual options (merged with default options)
  153. * @return this / result set (for chainable method calls on the result set)
  154. */
  155. $.fn.progressPie = function( options ) {
  156. //Note: Normally the @function directive for jsDoc should not contain the parentheses "()".
  157. //But I needed to add something to the name in order to be able to document the plugin function and its namespace
  158. //separately (though in reality both are the same).
  159. /*
  160. property: either opts.margin or opts.padding, may be number or array.
  161. i \elem {0..3} => 0: top, 1: right, 2: bottom, 3: left
  162. */
  163. function getMarginOrPaddingFromProp(property, i) {
  164. if (typeof property === 'number') {
  165. return property;
  166. } else if (Array.isArray(property) && property.length > 0) {
  167. if (property.length === 1) {
  168. return property[0];
  169. } else { // length > 1
  170. var j = i;
  171. while (j >= property.length) { // j > 1 ; j >= 2
  172. j -= 2; // j >= 0
  173. }
  174. // 0 <= j < length > 1 => array access valid
  175. return property[j];
  176. }
  177. } else {
  178. return 0;
  179. }
  180. }
  181. const optsMethods = {
  182. getMargin: function(i) {
  183. return getMarginOrPaddingFromProp(this.margin, i);
  184. },
  185. getPadding: function(i) {
  186. return getMarginOrPaddingFromProp(this.padding, i);
  187. }
  188. };
  189. // Extend our default options with those provided.
  190. // Note that the first argument to extend is an empty
  191. // object – this is to keep from overriding our "defaults" object.
  192. var globalOpts = $.extend( {}, $.fn.progressPie.defaults, options, optsMethods );
  193. var noargs = typeof options === "undefined";
  194. //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.
  195. //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
  196. //the forEach loop below...
  197. var NS = "http://www.w3.org/2000/svg";
  198. var contentPluginNS = "jQuery.fn.progressPie.contentPlugin";
  199. //Naming convention:
  200. // "self" is used to refer to the function object in order to simplify access to public members.
  201. // "me", on the other hand, may be used in functions to save the object reference "this" (or "$(this)").
  202. var self = $.fn.progressPie;
  203. var internalMode = $.extend( {USER_COLOR_CONST:{}, USER_COLOR_FUNC:{}, DATA_ATTR_FUNC:{}}, self.Mode );
  204. //private functions
  205. function createId(prefix) {
  206. if (typeof idCounter[prefix] === "undefined") {
  207. idCounter[prefix] = 0;
  208. }
  209. return prefix + (++idCounter[prefix]);
  210. }
  211. function angle(percent) {
  212. return 0.02 * Math.PI * percent; //2 Pi * percent / 100
  213. }
  214. function evalDataAttrFunc(functionString, percent) {
  215. var evalIndirect = eval;
  216. var handler = evalIndirect(functionString);
  217. if (typeof handler === "function") {
  218. return handler(percent);
  219. } else {
  220. throw new Error("The value of the colorFunctionAttr attribute is NOT a function: " + functionString);
  221. }
  222. }
  223. function evalContentPluginName(name) {
  224. var evalIndirect = eval;
  225. var f = evalIndirect(contentPluginNS + "." + name);
  226. if (typeof f === "function" || typeof f === 'object' && typeof f.draw === 'function') {
  227. return f;
  228. } else {
  229. throw new Error(name + " is not the name of a function or object in namespace " + contentPluginNS + "!");
  230. }
  231. }
  232. function getContentPlugin(property) {
  233. var f;
  234. if (typeof property === 'function' || typeof property === 'object' && typeof property.draw === 'function') {
  235. f = property;
  236. } else if (typeof property === 'string') {
  237. f = evalContentPluginName(property);
  238. } else {
  239. 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 + "!");
  240. }
  241. return f;
  242. }
  243. function getContentPlugins(property) {
  244. if (Array.isArray(property)) {
  245. return $.map(property, getContentPlugin);
  246. } else {
  247. return [getContentPlugin(property)];
  248. }
  249. }
  250. function getContentPluginOptions(property, i) {
  251. if (property === null) {
  252. return null;
  253. } else if (Array.isArray(property)) {
  254. return property[i];
  255. } else if (i === 0 && typeof property === "object") {
  256. return property;
  257. } else {
  258. return null;
  259. }
  260. }
  261. function getArcLength(rad, percent) {
  262. return 0.02 * Math.PI * rad * percent; //2πr * percent/100 = 0.02πr * percent
  263. }
  264. function addAnimationFromTo(target, attrName, attrType, from, to, animationAttrs) {
  265. var anim = document.createElementNS(NS, "animate");
  266. anim.setAttribute("attributeName", attrName);
  267. anim.setAttribute("attributeType", attrType);
  268. anim.setAttribute("from", from);
  269. anim.setAttribute("to", to);
  270. anim.setAttribute("fill", "freeze"); //when the animation stops, it's final state shall persist.
  271. for (var key in animationAttrs) {
  272. anim.setAttribute(key, animationAttrs[key]);
  273. }
  274. target.appendChild(anim);
  275. }
  276. function setStrokeDashArray(circle, strokeDashes, circumference) {
  277. var cnt;
  278. var len;
  279. if (typeof strokeDashes === 'number') {
  280. cnt = strokeDashes;
  281. } else if (typeof strokeDashes === 'object') {
  282. cnt = strokeDashes.count;
  283. len = strokeDashes.length;
  284. } else {
  285. throw new Error("illegal option: 'strokeDashes' is neither number (count) nor object!");
  286. }
  287. if (typeof cnt === 'undefined') {
  288. throw new Error("illegal option: 'strokeDashes' does not specify the 'count' property!");
  289. }
  290. if (typeof len === 'undefined') {
  291. //default: strokes and gaps equally long
  292. len = circumference / cnt / 2;
  293. } else if (typeof len === 'string') {
  294. len = len.trim();
  295. var percent = len.substring(len.length - 1) === '%';
  296. len = Number.parseInt(len, 10);
  297. if (percent) {
  298. len = circumference * len / 100;
  299. }
  300. }
  301. if (len * cnt >= circumference) {
  302. throw new Error("Illegal options: strokeDashCount * strokeDashLength >= circumference, can't set stroke-dasharray!");
  303. } else {
  304. var gap = (circumference - len * cnt) / cnt;
  305. var offset = typeof strokeDashes === 'object' && strokeDashes.centered ? 1.0 * len / 2 : 0;
  306. if (typeof strokeDashes !== 'object' || !strokeDashes.inverted) {
  307. circle.style.strokeDasharray = "" + len + "px, " + gap + "px";
  308. if (offset !== 0) {
  309. circle.style.strokeDashoffset = "" + offset + "px";
  310. }
  311. } else {
  312. circle.style.strokeDasharray = "" + gap + "px, " + len + "px";
  313. circle.style.strokeDashoffset = "" + (gap + offset) + "px";
  314. }
  315. }
  316. }
  317. function addTitle(node, title) {
  318. if (typeof title === "string") {
  319. const t = document.createElementNS(NS, "title");
  320. $(t).text(title);
  321. node.appendChild(t);
  322. }
  323. }
  324. function drawPie(svg, defs, rad, strokeWidth, strokeColor, strokeDashes, strokeFill, overlap, ringWidth, ringEndsRounded, ringAlign, cssClassBackgroundCircle, cssClassForegroundPie, percent, prevPercent, color, prevColor, title, animationAttrs, rotation) {
  325. //strokeWidth or ringWidth must not be greater than the radius:
  326. if (typeof strokeWidth === 'number') {
  327. strokeWidth = Math.min(strokeWidth, rad);
  328. }
  329. if (typeof ringWidth === 'number') {
  330. ringWidth = Math.min(ringWidth, rad);
  331. }
  332. var ringAlignRad = -1;
  333. if (typeof strokeWidth === 'number' && typeof ringWidth === 'number' && strokeWidth > 0 && ringWidth > 0 &&
  334. strokeWidth !== ringWidth && overlap && (ringAlign === self.RingAlign.CENTER || ringAlign === self.RingAlign.INNER)) {
  335. //pre-calculate ringAlignRad for ring mode in case ringWidth and strokeWidth differ (and are both > 0)
  336. //and the ringAlign option INNER or CENTER is set.
  337. //This value ringAlignRad then denotes the radius for the "smaller" (with slimmer stroke) of the two circles.
  338. //Depending on whose stroke-width is smaller, this will either be applied to the background circle
  339. //or to the ring. This decision is made later.
  340. var maxw = Math.max(ringWidth, strokeWidth);
  341. var minw = Math.min(ringWidth, strokeWidth);
  342. ringAlignRad = ringAlign === self.RingAlign.CENTER ? rad - (maxw / 2) : rad - maxw + (minw / 2);
  343. }
  344. var r;
  345. var circle;
  346. var strokeColorConfigured = false; //default value
  347. //1. background Circle
  348. // (now always drawn, even with strokeWidth==0, with CSS class allowing
  349. // to set the CSS fill property for the background)
  350. // (not drawn with undefined strokeWidth, which is the usual value for inner pies)
  351. if (typeof strokeWidth === 'number') {
  352. circle = document.createElementNS(NS, "circle");
  353. circle.setAttribute("cx", 0);
  354. circle.setAttribute("cy", 0);
  355. if (ringAlignRad > 0 && strokeWidth < ringWidth) {
  356. r = ringAlignRad;
  357. } else {
  358. r = rad - strokeWidth / 2;
  359. if (!overlap && ringAlign === self.RingAlign.INNER) {
  360. r -= ringWidth;
  361. }
  362. }
  363. circle.setAttribute("r", r);
  364. //Starting point of a circle's stroke is 3 o'clock by default.
  365. //Normally this point is invisible, but it might get visible if a stroke-dasharray is set
  366. //(which the user can do at any time via CSS):
  367. //Then this point where the stroke starts end ends is at 3 o'clock, but it should be at 12 o'clock,
  368. //since that's also the starting/ending point of the pie charts. Therefore, the circle will be
  369. //rotated 90 degrees anti-clockwise:
  370. circle.setAttribute("transform", "rotate(-90)");
  371. if (strokeDashes) {
  372. setStrokeDashArray(circle, strokeDashes, 2.0 * Math.PI * r);
  373. }
  374. strokeColorConfigured = typeof strokeColor === 'string';
  375. let stroke = strokeColorConfigured ? strokeColor : color;
  376. if (typeof stroke === "string") {
  377. circle.style.stroke = stroke;
  378. //In case of color animation this may be overwritten later on...
  379. }
  380. if (typeof strokeFill === "string") {
  381. circle.style.fill = strokeFill;
  382. }
  383. circle.style.strokeWidth = strokeWidth;
  384. circle.setAttribute("class", cssClassBackgroundCircle);
  385. addTitle(circle, title);
  386. svg.appendChild(circle);
  387. }
  388. const sw = ringWidth ? ringWidth : (overlap || typeof strokeWidth !== 'number') ? rad : rad - strokeWidth;
  389. if (ringAlignRad > 0 && ringWidth < strokeWidth) {
  390. //reduce ring radius for INNER or CENTER alignment with wider background circle's stroke
  391. //Note: ringAlignRad > 0 implies ringWidth and strokeWidth to be defined and > 0...
  392. r = ringAlignRad;
  393. } else {
  394. //ring radius max (except if to avoid overlap with outer background circle)
  395. r = rad - sw / 2;
  396. if (!overlap && typeof strokeWidth === 'number' && ringAlign === self.RingAlign.OUTER) {
  397. r -= strokeWidth;
  398. }
  399. }
  400. if (percent === 100 && !animationAttrs && typeof color === "string") {
  401. //Simply draw filled circle. (Not in CSS color mode, not with animation activated.)
  402. //"value" circle (full pie or ring)
  403. const circle2 = document.createElementNS(NS, "circle");
  404. circle2.setAttribute("cx", 0);
  405. circle2.setAttribute("cy", 0);
  406. circle2.setAttribute("r", r);
  407. circle2.style.stroke = color;
  408. circle2.style.strokeWidth = sw;
  409. circle2.style.fill = "none";
  410. circle2.setAttribute("class", cssClassForegroundPie);
  411. addTitle(circle2, title);
  412. svg.appendChild(circle2);
  413. } else if (percent > 0 && percent < 100 || (animationAttrs || typeof color === "undefined") && (percent === 0 || percent === 100)) {
  414. //2. Pie (or ring)
  415. const arc = document.createElementNS(NS, "path");
  416. let arcToPercent = percent;
  417. //Before calculating the arc's path, first evaluate the optional animation.
  418. //Reason: For backwards animation, the arc has to span to the previous value instead
  419. // to the real target value, and only the delta part of it will be (animatedly)
  420. // made invisible via stroke dash properties.
  421. // I.e. the arcToPercent value may be overwritten in the following block:
  422. if (animationAttrs) {
  423. var delta = percent - prevPercent;
  424. var deltaArcLen = getArcLength(r, delta);
  425. var backwards = delta < 0;
  426. var animFrom;
  427. var animTo;
  428. if (backwards) {
  429. arcToPercent = prevPercent;
  430. animFrom = "0px";
  431. animTo = -deltaArcLen + "px";
  432. } else {
  433. animFrom = deltaArcLen + "px";
  434. animTo = "0px";
  435. }
  436. var arcLen = getArcLength(r, arcToPercent);
  437. arc.setAttribute("stroke-dasharray", arcLen + "px " + arcLen + "px");
  438. arc.setAttribute("stroke-dashoffset", animFrom);
  439. //Setting the "static image" to the animFrom value, i.e. to the state of the image *before animation starts*,
  440. //a) requires the fill="freeze" attribute in order to finally (after animation) show the correct state (animTo).
  441. //b) ensures smooth animation without flicker (setting this attribute to animTo causes some browsers to display
  442. // the target state (animTo) for a slit second bevor animation starts, which can look irritating),
  443. //c) requires a SMIL detection fork (see smilSupported()): Since Browsers without SMIL support will display this static image and
  444. // never replace it with the animation's end state, setting this stroke-dashoffset attribute must only be
  445. // executed in browsers with SMIL support!
  446. // Setting this to animFrom would be compatible with no-SMIL-browsers, but for the price of said flicker.
  447. //=> This function (in this state) requires animationAttrs to be falsy if smilSupported() === false, see function call!
  448. addAnimationFromTo(arc, "stroke-dashoffset", "CSS", animFrom, animTo, animationAttrs);
  449. //Remove linecap when reduced to 0 percent!
  450. if (ringEndsRounded && percent === 0) {
  451. addAnimationFromTo(arc, "stroke-linecap", "CSS", "round", "butt", animationAttrs);
  452. }
  453. //Color Animation?
  454. if (prevColor && prevColor !== color) {
  455. addAnimationFromTo(arc, "stroke", "CSS", prevColor, color, animationAttrs);
  456. //Apply to outer circle's stroke?
  457. if (!strokeColorConfigured && circle) {
  458. circle.style.stroke = prevColor;
  459. addAnimationFromTo(circle, "stroke", "CSS", prevColor, color, animationAttrs);
  460. }
  461. }
  462. }
  463. const alpha = angle(arcToPercent);
  464. //Special case 100% (only in animated mode): targetX must not be 0: Arc won't be visible
  465. //if start and end point are identical. Move end point minimally to the left.
  466. //(Gap should not be visible if the graphic does not get scaled up too much.)
  467. const targetX = arcToPercent === 100 ? -0.00001 : Math.sin(alpha)*r;
  468. const targetY = Math.cos(alpha-Math.PI)*r;
  469. const largeArcFlag = arcToPercent > 50 ? "1" : "0";
  470. const clockwiseFlag = "1";
  471. const starty = -r;
  472. //start
  473. let path = "M0,"+starty;
  474. //arc
  475. path += " A"+r+","+r+" 0 "+largeArcFlag+","+clockwiseFlag+" "+targetX+","+targetY;
  476. arc.setAttribute("d", path);
  477. arc.style.fill = "none";
  478. if (typeof color === "string") {
  479. arc.style.stroke = color;
  480. }
  481. arc.style.strokeWidth = sw;
  482. arc.style.strokeLinecap = ringEndsRounded && percent > 0 ? "round" : "butt";
  483. if (rotation) {
  484. //rotation is "truthy".
  485. //May be "true" or a String (i.e. duration) or an object holding properties "duration" and "clockwise".
  486. const rotationStyleID="progresspie-rotation-style";
  487. const rotationName="progresspie-rotate";
  488. const rotationStyle="@keyframes "+ rotationName + " {100% {transform: rotate(360deg);}}";
  489. if (!$("#" + rotationStyleID).length) {
  490. var head = $("head");
  491. if (head.length) {
  492. const style = document.createElement("style");
  493. style.id = rotationStyleID;
  494. $(style).text(rotationStyle);
  495. head.get(0).appendChild(style);
  496. } else {
  497. const style = document.createElementNS(NS, "style");
  498. style.id = rotationStyleID;
  499. $(style).text(rotationStyle);
  500. defs.appendChild(style);
  501. }
  502. }
  503. const anticlockwise = rotation.clockwise === false;
  504. const dur = typeof rotation === "string" ? rotation :
  505. typeof rotation.duration === "string" ? rotation.duration :
  506. "1s"; //Default duration for true or any other truthy value is 1 second.
  507. // const anim = document.createElementNS(NS, "animateTransform");
  508. // anim.setAttribute("attributeName", "transform");
  509. // anim.setAttribute("attributeType", "XML");
  510. // anim.setAttribute("type", "rotate");
  511. // anim.setAttribute("from", "0");
  512. // anim.setAttribute("to", anticlockwise ? "-360" : "360");
  513. // anim.setAttribute("dur", dur);
  514. // anim.setAttribute("repeatDur", "indefinite");
  515. // arc.appendChild(anim);
  516. const timing = typeof rotation.timing === "string" ? rotation.timing : "linear";
  517. arc.style.animation = rotationName + " " + dur + " " + timing
  518. + (anticlockwise ? " reverse" : "") + " infinite";
  519. }
  520. arc.setAttribute("class", cssClassForegroundPie);
  521. addTitle(arc, title);
  522. svg.appendChild(arc);
  523. }
  524. }
  525. function getRawValueStringOrNumber(me, opts) {
  526. var stringOrNumber;
  527. const vi = getValueInputObject(opts);
  528. if (vi !== null) {
  529. stringOrNumber = vi.val();
  530. if (typeof opts.valueData !== "undefined" || typeof opts.valueAttr !== "undefined" || typeof opts.valueSelector !== "undefined") {
  531. throw new Error("options 'valueInput', 'valueData', 'valueAttr' and 'valueSelector' are mutually exclusive, i.e. at least three must be undefined!");
  532. }
  533. } else if (typeof opts.valueData === "string") {
  534. stringOrNumber = me.data(opts.valueData);
  535. if (typeof opts.valueAttr !== "undefined" || typeof opts.valueSelector !== "undefined") {
  536. throw new Error("options 'valueData', 'valueAttr' and 'valueSelector' are mutually exclusive, i.e. at least two must be undefined!");
  537. }
  538. } else if (typeof opts.valueData !== "undefined") {
  539. throw new Error("option 'valueData' is not of type 'string'!");
  540. } else if (typeof opts.valueAttr === "string") {
  541. stringOrNumber = me.attr(opts.valueAttr);
  542. if (typeof opts.valueSelector !== "undefined") {
  543. throw new Error("options 'valueAttr' and 'valueSelector' are mutually exclusive, i.e. at least one must be undefined!");
  544. }
  545. } else if (typeof opts.valueAttr !== "undefined") {
  546. throw new Error("option 'valueAttr' is not of type 'string'!");
  547. } else if (typeof opts.valueSelector !== "undefined") {
  548. stringOrNumber = $(opts.valueSelector, me).text();
  549. }
  550. if (typeof stringOrNumber === "undefined") {
  551. stringOrNumber = me.text();
  552. }
  553. return stringOrNumber;
  554. }
  555. function getPercentValue(rawValueStringOrNumber, opts) {
  556. return Math.max(0, Math.min(100, opts.valueAdapter(rawValueStringOrNumber)));
  557. }
  558. function getModeAndColor(me, opts) {
  559. var mode = opts.mode;
  560. var color = opts.color;
  561. if (mode === self.Mode.CSS) {
  562. color = undefined;
  563. } else {
  564. //color may be a function or a constant
  565. var ct = typeof color;
  566. if (ct !== "undefined" && ct !== "string" && ct !== "function") {
  567. throw new Error("option 'color' has to be either a function or a string, but is of type '" + ct + "'!");
  568. }
  569. if (ct === 'function') {
  570. mode = internalMode.USER_COLOR_FUNC;
  571. } else {
  572. if (ct === 'undefined' && typeof opts.colorAttr === "string") {
  573. color = me.attr(opts.colorAttr);
  574. }
  575. if (typeof color === 'string') {
  576. mode = internalMode.USER_COLOR_CONST;
  577. } else if (typeof opts.colorFunctionAttr === "string") {
  578. color = me.attr(opts.colorFunctionAttr);
  579. if (typeof color === 'string') {
  580. mode = internalMode.DATA_ATTR_FUNC;
  581. }
  582. }
  583. }
  584. }
  585. return {mode: mode, color: color};
  586. }
  587. function calcColor(mode, userdefinedPieColor, percent) {
  588. return mode === internalMode.CSS ? undefined :
  589. mode === internalMode.MASK ? internalMode.MASK.color :
  590. mode === internalMode.IMASK ? internalMode.IMASK.color :
  591. mode === internalMode.GREY ? internalMode.GREY.color :
  592. mode === internalMode.GREEN ? self.colorByPercent(100) :
  593. mode === internalMode.RED ? self.colorByPercent(0) :
  594. mode === internalMode.COLOR || userdefinedPieColor === undefined ? self.colorByPercent(percent) :
  595. mode === internalMode.USER_COLOR_CONST ? userdefinedPieColor :
  596. mode === internalMode.USER_COLOR_FUNC ? userdefinedPieColor(percent) :
  597. mode === internalMode.DATA_ATTR_FUNC ? evalDataAttrFunc(userdefinedPieColor, percent)
  598. : "black";
  599. }
  600. function calcFill(mode, opts, percent) {
  601. return mode === internalMode.CSS /*|| mode === internalMode.MASK */ ? undefined :
  602. typeof opts.backgroundColor === "string" ? opts.backgroundColor :
  603. typeof opts.backgroundColor === "function" ? opts.backgroundColor(percent) :
  604. mode === internalMode.IMASK ? internalMode.MASK.color :
  605. "none";
  606. }
  607. function ctPluginIsFullSize(opts, pluginOpts) {
  608. return typeof opts.ringWidth === "undefined" || pluginOpts && pluginOpts.fullSize;
  609. }
  610. function drawRect(targetNode, rad, paddingProp, stroke, fill, strokeWidth) {
  611. var rect = document.createElementNS(NS, "rect");
  612. targetNode.appendChild(rect);
  613. var left = rad + getMarginOrPaddingFromProp(paddingProp, 3);
  614. var top = rad + getMarginOrPaddingFromProp(paddingProp, 0);
  615. var width = left + rad + getMarginOrPaddingFromProp(paddingProp, 1);
  616. var height = top + rad + getMarginOrPaddingFromProp(paddingProp, 2);
  617. if (typeof strokeWidth === "number" && stroke !== "none") {
  618. rect.setAttribute("stroke-width", strokeWidth);
  619. width -= strokeWidth;
  620. height -= strokeWidth;
  621. left -= strokeWidth / 2;
  622. top -= strokeWidth / 2;
  623. }
  624. rect.setAttribute("x", "-" + left);
  625. rect.setAttribute("y", "-" + top);
  626. rect.setAttribute("width", width);
  627. rect.setAttribute("height", height);
  628. rect.setAttribute("stroke", stroke);
  629. rect.setAttribute("fill", fill);
  630. }
  631. function createSVG(rad, opts) {
  632. var svg = document.createElementNS(NS, "svg");
  633. var leftWidth = rad + opts.getPadding(3) + opts.getMargin(3);
  634. var topHeight = rad + opts.getPadding(0) + opts.getMargin(0);
  635. var totalWidth = leftWidth + rad + opts.getPadding(1) + opts.getMargin(1);
  636. var totalHeight = topHeight + rad + opts.getPadding(2) + opts.getMargin(2);
  637. var scaledWidth = totalWidth;
  638. var scaledHeight = totalHeight;
  639. if (typeof opts.scale === "number") {
  640. scaledWidth *= opts.scale;
  641. scaledHeight *= opts.scale;
  642. }
  643. svg.setAttribute("width", Math.ceil(scaledWidth));
  644. svg.setAttribute("height", Math.ceil(scaledHeight));
  645. svg.setAttribute("viewBox", "-" + leftWidth + " -" + topHeight + " " + totalWidth + " " + totalHeight);
  646. return svg;
  647. }
  648. /**
  649. * returns raw and percent value (as well as previous value and more)
  650. * and updates options by application of optionsByRawValue() or optionsByPercent()
  651. * functions present in options.
  652. * I.e. the options object may be extended.
  653. * Returns object with values.
  654. */
  655. function getValuesAndUpdateOpts(me, opts, innerLevel) {
  656. var out = {};
  657. out.raw = getRawValueStringOrNumber(me, opts);
  658. if (typeof opts.optionsByRawValue === "function") {
  659. var newOptsRaw = opts.optionsByRawValue(out.raw);
  660. if (typeof newOptsRaw !== "undefined" && newOptsRaw !== null) {
  661. $.extend(opts, newOptsRaw);
  662. //Update raw in case newOptsRaw defines new value data selector
  663. out.raw = getRawValueStringOrNumber(me, opts);
  664. }
  665. }
  666. out.p = getPercentValue(out.raw, opts);
  667. var prevDataName = innerLevel === 0 ? self.prevValueDataName : self.prevInnerValueDataName;
  668. if (innerLevel > 1) {
  669. prevDataName += innerLevel;
  670. }
  671. out.prevP = me.data(prevDataName);
  672. out.isInitialValue = typeof out.prevP === 'undefined';
  673. me.data(prevDataName, out.p);
  674. if (typeof out.prevP !== 'number') {
  675. out.prevP = 0;
  676. }
  677. if (typeof opts.optionsByPercent === "function") {
  678. var newOpts = opts.optionsByPercent(out.p);
  679. if (typeof newOpts !== "undefined" && newOpts !== null) {
  680. $.extend(opts, newOpts);
  681. //Update values in case the optionsByPercent define different value adapter functions or value data selectors
  682. out.raw = getRawValueStringOrNumber(me, opts);
  683. out.p = getPercentValue(out.raw, opts);
  684. }
  685. }
  686. return out;
  687. }
  688. $(this).each(function () {
  689. const me = $(this);
  690. let opts = $.extend({}, globalOpts);
  691. if (noargs) {
  692. var localOpts = $(this).data($.fn.setupProgressPie.dataKey);
  693. if (typeof localOpts === "object") {
  694. opts = $.extend({}, localOpts, optsMethods); //use stored individual setup instead of gobalOpts (which in this case (noargs) are just defaults anyway).
  695. }
  696. }
  697. var existing = $("svg", me); //existing SVGs in target element
  698. if (!existing.length || opts.update) { //Only draw if no SVG already existing or update mode
  699. if (existing.length && opts.update) { //remove existing SVG
  700. existing.remove();
  701. opts.separator = ''; //reset any separator when applying an update in order not to repeatedly insert a new one with each update.
  702. }
  703. var values = getValuesAndUpdateOpts(me, opts, 0);
  704. var h = Math.ceil(typeof opts.size === "number" ? opts.size : me.height());
  705. if (h === 0) {
  706. h = 20;
  707. }
  708. h *= opts.sizeFactor;
  709. var rad = h / 2;
  710. var totalRad = rad;
  711. var mc = getModeAndColor(me, opts);
  712. //Note: mc.mode is an internal mode which gets replaced by internal mode constants in case
  713. //user defined colors or color functions should be applied. The function calcColor/calcFill
  714. //should be called with mc.mode.
  715. //But since in combinations of MASK or IMASK mode with user-defined color functions,
  716. //mc.mode will take an internal mode reflecting how to calculate the color and loose the
  717. //information of the mask mode, any check not calculating the color but the draw mode
  718. //(i.e. decide whether to draw a mask or a visible diagram element) has to be based
  719. //on the original opts.mode instead of mc.mode!
  720. var color = calcColor(mc.mode, mc.color, values.p);
  721. var fill = calcFill(mc.mode, opts, values.p);
  722. var prevColor;
  723. if (opts.animateColor === true || typeof opts.animateColor === "undefined" && !values.isInitialValue) {
  724. prevColor = calcColor(mc.mode, mc.color, values.prevP);
  725. }
  726. var animationAttrs = !self.smilSupported() ? null
  727. : opts.animate === true ? self.defaultAnimationAttributes
  728. : typeof opts.animate === 'object' ? $.extend({}, self.defaultAnimationAttributes, opts.animate)
  729. : null;
  730. //Check for content plug-in and whether the pie chart is to be drawn at all:
  731. var ctPlugins = null;
  732. var hideChart = false;
  733. var ctPluginBaseCheckArgs = {
  734. isFullSize: function() {
  735. return ctPluginIsFullSize(opts, this);
  736. },
  737. isCssMode: function() {
  738. return typeof this.color !== "string";
  739. },
  740. color: color,
  741. percentValue: values.p,
  742. rawValue: values.raw,
  743. pieOpts: opts
  744. };
  745. if (opts.contentPlugin) {
  746. ctPlugins = getContentPlugins(opts.contentPlugin);
  747. for (var pluginIndex = 0; pluginIndex < ctPlugins.length; pluginIndex++) {
  748. var ctPlugin = ctPlugins[pluginIndex];
  749. var ctpOpts = getContentPluginOptions(opts.contentPluginOptions, pluginIndex);
  750. var checkArgs = ctPluginBaseCheckArgs;
  751. if (ctpOpts !== null && typeof ctpOpts === "object") {
  752. checkArgs = $.extend({}, ctPluginBaseCheckArgs, ctpOpts);
  753. }
  754. if (typeof ctPlugin === 'object' && typeof ctPlugin.hidesChartIfFullSize === 'function') {
  755. hideChart = hideChart ||
  756. (opts.mode !== self.Mode.MASK && //in MASK mode the chart is a mask and cannot be hidden by content!
  757. opts.mode !== self.Mode.IMASK &&
  758. ctPluginIsFullSize(opts, ctpOpts) &&
  759. ctPlugin.hidesChartIfFullSize(checkArgs));
  760. }
  761. }
  762. }
  763. //Create and insert SVG...
  764. var svg = createSVG(rad, opts);
  765. var defs = document.createElementNS(NS, "defs");
  766. if (mc.mode !== self.Mode.CSS) {
  767. svg.style.verticalAlign = opts.verticalAlign;
  768. }
  769. if (me.is(":empty")) { //simply insert (regardless of prepend option, and without separator)
  770. me.append(svg);
  771. } else if (opts.prepend) {
  772. me.prepend(svg, opts.separator);
  773. } else {
  774. me.append(opts.separator, svg);
  775. }
  776. //Optionally add title to SVG:
  777. addTitle(svg, opts.globalTitle);
  778. //Draw/insert Pie
  779. var maskId = null;
  780. var chartTargetNode = svg;
  781. if (!hideChart) {
  782. if (opts.mode === self.Mode.MASK || opts.mode === self.Mode.IMASK) {
  783. chartTargetNode = document.createElementNS(NS, "mask");
  784. defs.appendChild(chartTargetNode);
  785. maskId = createId("pie");
  786. chartTargetNode.setAttribute("id", maskId);
  787. if (opts.mode === self.Mode.IMASK) {
  788. //fill the background behind the black pie with a white rectangle to complete the inverted mask:
  789. drawRect(chartTargetNode, rad, opts.padding, "none", fill);
  790. }
  791. }
  792. var cssForeground = opts.cssClassForegroundPie;
  793. var cssBackground = opts.cssClassBackgroundCircle;
  794. if (typeof opts.inner === 'object') {
  795. cssForeground += " " + opts.cssClassOuter;
  796. cssBackground += " " + opts.cssClassOuter;
  797. }
  798. 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);
  799. }
  800. //w: ringWidth of innermost ring to calculate free disc inside avaliable for content plug-in.
  801. var w = typeof opts.ringWidth === 'number' ? opts.ringWidth : typeof opts.strokeWidth === 'number' ? opts.strokeWidth : 0;
  802. //Draw a second, inner pie?
  803. var inner = opts.inner;
  804. var innerCnt = 0;
  805. while (typeof inner === 'object') {
  806. innerCnt++;
  807. inner = $.extend({}, inner); //make copy before modifications
  808. if (typeof inner.valueAdapter === "undefined") {
  809. inner.valueAdapter = self.defaults.valueAdapter;
  810. }
  811. if (typeof inner.overlap === 'undefined') {
  812. inner.overlap = self.defaults.overlap;
  813. }
  814. if (typeof inner.ringAlign === 'undefined') { //inherit from outer
  815. inner.ringAlign = opts.ringAlign; //(must not be undefined)
  816. }
  817. var innerValues = getValuesAndUpdateOpts(me, inner, innerCnt);
  818. var cssClassName = opts.cssClassInner;
  819. if (innerCnt > 1) {
  820. cssClassName += innerCnt;
  821. }
  822. mc = getModeAndColor(me, inner);
  823. rad = typeof inner.size === "number" ? inner.size * opts.sizeFactor / 2 : rad * 0.6;
  824. var innerColor = calcColor(mc.mode, mc.color, innerValues.p);
  825. var innerPrevColor = null;
  826. if (inner.animateColor === true || typeof inner.animateColor === "undefined" && (opts.animateColor === true || typeof opts.animateColor === "undefined" && innerValues.isInitialValue)) {
  827. innerPrevColor = calcColor(mc.mode, mc.color, innerValues.prevP);
  828. }
  829. if (inner.animate === false || !self.smilSupported()) {
  830. animationAttrs = null;
  831. } else if (inner.animate === true && animationAttrs === null) {
  832. animationAttrs = self.defaultAnimationAttributes;
  833. } else if (typeof inner.animate === "object") {
  834. if (animationAttrs === null) {
  835. animationAttrs = $.extend({}, self.defaultAnimationAttributes, inner.animate);
  836. } else {
  837. animationAttrs = $.extend({}, animationAttrs, inner.animate);
  838. }
  839. }
  840. if (!hideChart) {
  841. 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);
  842. }
  843. w = typeof inner.ringWidth === 'number' ? inner.ringWidth : 0;
  844. inner = inner.inner;
  845. }
  846. if (ctPlugins !== null) {
  847. var r = rad;
  848. if (w < rad) {
  849. r -= w;
  850. }
  851. var baseArgs = $.extend({
  852. newSvgElement: function(name) {
  853. var el = document.createElementNS(NS, name);
  854. group.appendChild(el);
  855. return el;
  856. },
  857. newSvgSubelement: function(parent, name) {
  858. var el = document.createElementNS(NS, name);
  859. parent.appendChild(el);
  860. return el;
  861. },
  862. newDefElement: function(name) {
  863. var el = document.createElementNS(NS, name);
  864. defs.appendChild(el);
  865. return el;
  866. },
  867. createId: createId,
  868. getBackgroundRadius: function(ignoreMargin) {
  869. var r = this.isFullSize() ? this.totalRadius: this.radius;
  870. if (! ignoreMargin) {
  871. var margin = typeof this.margin === "number" ? this.margin :
  872. this.isFullSize() ? this.pieOpts.defaultContentPluginBackgroundMarginFullSize
  873. : this.pieOpts.defaultContentPluginBackgroundMarginInsideRing;
  874. r -= margin;
  875. }
  876. return r;
  877. },
  878. addBackground: function(radius, cssClassName) {
  879. //fill background if set
  880. const classNameSet = typeof cssClassName === "string";
  881. if (this.backgroundColor || classNameSet) {
  882. var bg = this.newSvgElement("circle");
  883. bg.setAttribute("cx", "0");
  884. bg.setAttribute("cy", "0");
  885. bg.setAttribute("r", radius);
  886. if (this.backgroundColor) {
  887. bg.setAttribute("fill", this.backgroundColor);
  888. }
  889. if (classNameSet) {
  890. bg.setAttribute("class", cssClassName);
  891. }
  892. }
  893. },
  894. addBackgroundRect: function(stroke, fill, strokeWidth) {
  895. drawRect(group, totalRad, opts.padding, stroke, fill, strokeWidth);
  896. },
  897. getContentPlugin: getContentPlugin,
  898. radius: r,
  899. totalRadius: totalRad,
  900. color: color,
  901. percentValue: values.p,
  902. rawValue: values.raw
  903. }, ctPluginBaseCheckArgs);
  904. var maskNotAppliedYet = true;
  905. for (var pluginIndex2 = 0; pluginIndex2 < ctPlugins.length; pluginIndex2++) {
  906. var ctPlugin2 = ctPlugins[pluginIndex2];
  907. var group = document.createElementNS(NS, "g");
  908. var f = typeof ctPlugin2 === 'function' ? ctPlugin2 : ctPlugin2.draw;
  909. var args = baseArgs;
  910. var ctpOpts2 = getContentPluginOptions(opts.contentPluginOptions, pluginIndex2);
  911. if (ctpOpts2 !== null && typeof ctpOpts2 === 'object') {
  912. args = $.extend({}, baseArgs, ctpOpts2);
  913. }
  914. f(args);
  915. if (typeof ctPlugin2.inBackground === 'boolean' && ctPlugin2.inBackground ||
  916. typeof ctPlugin2.inBackground === 'function' && ctPlugin2.inBackground(args) ) {
  917. $(svg).prepend(group);
  918. if (maskId !== null && maskNotAppliedYet) {
  919. group.setAttribute("mask", "url(#" + maskId + ")");
  920. maskNotAppliedYet = false;
  921. }
  922. } else {
  923. $(svg).append(group);
  924. }
  925. }
  926. if (maskId !== null && maskNotAppliedYet) {
  927. throw new Error("MASK mode could not be applied since no content plug-in drew a background to be masked! " +
  928. "You need do specify at least one content plug-in which draws into the background!");
  929. }
  930. }
  931. if (defs.hasChildNodes()){
  932. $(svg).prepend(defs);
  933. }
  934. }
  935. });
  936. return this;
  937. };
  938. /**
  939. * Enum defining possible values for the <code>mode</code> option.
  940. * @memberOf jQuery.fn.progressPie
  941. * @enum
  942. * @readonly
  943. */
  944. $.fn.progressPie.Mode = {
  945. /** Default Mode: Pie is drawn in a shade of grey. The HTML color code is "#888" and may be changed by
  946. * overwriting <code>jQuery.fn.progressPie.Mode.GREY.color</code> (of type string).
  947. * @type {Object}
  948. */
  949. GREY:{color:"#888"},
  950. /** In mode RED the pie is drawn in red color regardless of the percentual value.
  951. * <code>jQuery.fn.progressPie.Mode.RED.value</code> is a variable of type "number" with the default value
  952. * of 200 and means the red color will be <code>rgb(200, 0, 0)</code>.
  953. * The variable RED.value is not only used in mode RED, but also in mode COLOR for calculating the
  954. * color of any value between 0 and 50.
  955. * @type {Object}
  956. */
  957. RED:{value:200},
  958. /** In mode GREEN the pie is drawn in green color regardless of the percentual value.
  959. * <code>jQuery.fn.progressPie.Mode.GREEN.value</code> is a variable of type "number" with the default value
  960. * of 200 and means the green color will be <code>rgb(0, 200, 0)</code>.
  961. * The variable GREEN.value is not only used in mode GREE, but also in mode COLOR for calculating the
  962. * color of any value between 50 and 100.
  963. * @type {Object}
  964. */
  965. GREEN:{value:200},
  966. /** In mode COLOR the color of the pie is depending on the percentual value.
  967. * The color is calculated via $.fn.progressPie".colorByPercent.
  968. * It's the same green color as in mode GREEN for a value of 100 percent, the same red color
  969. * as in mode RED for a value of 0%.
  970. * The colors may be altered by overwriting progressPie.Mode.RED.value or progressPie.Mode.GREEN.value.
  971. * @type {Object}
  972. */
  973. COLOR:{},
  974. /** In mode CSS the color style properties {@code stroke} and {@code fill} of the background circle
  975. * and the {@code stroke} property of the foreground (pie or ring) are not set at all and are
  976. * required to be set via CSS rules by the user. (The {@code fill} style of the foreground
  977. * is always set to 'none', even in CSS mode.)
  978. */
  979. CSS:{},
  980. /**
  981. * Mask Mode: Requires the use of a content plug-in which adds <em>background</em> content, otherwise
  982. * your chart will stay completely invisible!
  983. * If a chart calls one or more content plug-ins which add background content (the pie will be
  984. * used as a mask applied to the topmost background layer (i.e. the output of the first background
  985. * content plug-in). In the mask, the areas covered by the pie will be drawn in color
  986. * <code>jQuery.fn.progressPie.Mode.MASK.color</code> which defaults to <code>'white'</code>, while all the
  987. * areas not covered by the chart stay transparent. Effectively this default means that only those
  988. * parts of the background layer that would normally be covered by the chart will stay visible
  989. * while the rest of the background image will be clipped away.
  990. * <p>MASK mode may be combined with options like <code>color</code>, <code>strokeColor</code>
  991. * and <code>backgroundColor</code> in order to change the mask's transparency for the pie,
  992. * it's background circle and the filling of the latter.</p>
  993. * <p>(See examples page for content plug-ins
  994. * demonstrating the MASK mode with background images.)
  995. * @type {Object}
  996. */
  997. MASK:{color: "white"},
  998. /**
  999. * Inverted Mask Mode: Very similar to MASK mode, only the mask is inverted: By default,
  1000. * the image areas covered by the pie will not show the background but all the rest of the image,
  1001. * the chart will be "cut out of" the background layer.
  1002. * <p>This is achieved by drawing the pie in a mask layer with color
  1003. * <code>jQuery.fm.progressPie.Mode.IMASK.color</code>, which defaults to <code>'black'</code>
  1004. * on a background rectangle filled in color
  1005. * <code>jQuery.fm.progressPie.Mode.MASK.color</code>, which defaults to <code>'white'</code>.
  1006. * <p>IMASK mode may be combined with options like <code>color</code>, <code>strokeColor</code>
  1007. * and <code>backgroundColor</code>, with <code>color</code> overriding the pie's mask color
  1008. * (defaulting to black) and <code>background</code> overriding the pie's fill color (defaulting
  1009. * to white) <em>and</em> the filling of the background rectangle.
  1010. * The latter makes the difference in comparison to just using MASK mode and switching
  1011. * the <code>color</code> and <code>backgroundColor</code> value: In MASK mode,
  1012. * <code>backgroundColor</code> only defines a fill color for the inside of the circular chart,
  1013. * in IMASK mode the whole rectangular chart area (including the padding, if set) will be filled!
  1014. * </p>
  1015. */
  1016. IMASK:{color: "black"}
  1017. };
  1018. /**
  1019. * public static method to calculate a color for a percent value: green for 100%, red for 0%, yellow for 50%,
  1020. * gradients for values in between.
  1021. * This is used internally in mode progressPie.Mode.COLOR
  1022. * @param {number} percent - a value between 0 and 100 (inclusive). 0 results in red color, 100 in green, 50 in yellow,
  1023. * any other value greater than 50 generates a gradient between green and yellow, values less than 50 a gradient
  1024. * between red and yellow.
  1025. * @param {number} alpha - optional second parameter. If defined, the calculated color will not be a fully opaque
  1026. * <code>rgb(r,g,b)</code> value, but instead <code>rgba(r,g,b,alpha)</code>. Has to be a number between 0 and 1 inclusive
  1027. * and defines the transparency (0 for fully transparent, 1 for fully opaque).
  1028. * @memberOf jQuery.fn.progressPie
  1029. * @function colorByPercent
  1030. */
  1031. $.fn.progressPie.colorByPercent = function(percent, alpha) {
  1032. var maxGreen = $.fn.progressPie.Mode.GREEN.value;
  1033. var maxRed = $.fn.progressPie.Mode.RED.value;
  1034. var green = percent > 50 ? maxGreen : Math.floor(maxGreen * percent / 50);
  1035. var red = percent < 50 ? maxRed : Math.floor(maxRed * (100 - percent) / 50);
  1036. var rgb = red + "," + green + ",0";
  1037. return typeof alpha === "number" ? "rgba(" + rgb + "," + alpha +")" : "rgb(" + rgb + ")";
  1038. };
  1039. $.fn.progressPie.smilSupported = function() {
  1040. if (typeof $.fn.progressPie.smilSupported.cache === "undefined") {
  1041. //Test taken from Modernizr Library (MIT License) with special thanks to that project.
  1042. //This one line is pretty much identical to Modernizr's SMIL test routine, but by extracting it from that library,
  1043. //I don't need the whole Modernizr Framework around that test. This one line is actually more compact than even the
  1044. //smallest Modernizr custom feature build (supporting only the SMIL test and not generating CSS classes),
  1045. //and by integrating it here, I don't introduce unnecessary library dependencies.
  1046. $.fn.progressPie.smilSupported.cache = /SVGAnimate/.test(document.createElementNS("http://www.w3.org/2000/svg", "animate").toString());
  1047. }
  1048. return $.fn.progressPie.smilSupported.cache;
  1049. };
  1050. /**
  1051. * Enum defining possible valus for the <code>ringAlign</code> option.
  1052. * @memberOf jQuery.fn.progressPie
  1053. * @enum
  1054. * @readonly
  1055. */
  1056. $.fn.progressPie.RingAlign = {
  1057. /**
  1058. * Both strokes (background circle and ring graph) are drawn on the
  1059. * outer edge of the (circular) chart. <br>
  1060. * If the <code>overlap</code> option is set to false, the outer edge of the ring
  1061. * will be aligned with the inner edge of the background circle, i.e. the ring
  1062. * will be drawn <code>inside</code> of the background circle.
  1063. */
  1064. OUTER: {},
  1065. /**
  1066. * In this mode, the ring is drawn centered on top of background circle
  1067. * (provided, the <code>overlap</code> option is not turned off). If the ring
  1068. * is wider, it will overlap the background circle equally to the outside and to the
  1069. * inside of the circle, if it is slimmer, it will be centered "inside" the background
  1070. * circle.
  1071. */
  1072. CENTER: {},
  1073. /**
  1074. * In this mode, both of the two stroke (background circle and ring graph) will align
  1075. * towards the center of the circle, i.e. share the same inner radius.<br>
  1076. * If the <code>overlap</code> option is set to false, the inner edge of the ring
  1077. * will be aligned with the outer edge of the background circle, i.e. the ring will
  1078. * be drawn around the background circle, the latter will be shrunk to fit into
  1079. * the ring.
  1080. */
  1081. INNER: {}
  1082. };
  1083. /**
  1084. * Default Options.
  1085. * This is a public (static) object in order to allow users to globally modify the defaults
  1086. * before using the plug-in.
  1087. * @memberOf jQuery.fn.progressPie
  1088. * @member defaults
  1089. * @property {Mode} mode - A value of the enum type Mode, defaults to $.fn.progressPie.Mode.GREY
  1090. * @property {number} margin - a single number or an array of up to four numbers, defaults to 0 (no margin at all).
  1091. * 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),
  1092. * 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.
  1093. * 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
  1094. * 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.
  1095. * 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
  1096. * 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.
  1097. * @property {number} padding - a single number or an array of up to four numbers, defaults to 0 (no padding at all).
  1098. * 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
  1099. * both are specified, they add up. A difference may only be seen when using certain content plug-ins (like the predefined <code>image</code> plug-in),
  1100. * 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
  1101. * (but not covering the margin).
  1102. * @property {number} strokeWidth - The default width of the background circle stroke, defaults to 2
  1103. * @property {boolean} overlap - if true (default), the foreground pie or ring fragment is drawn full size
  1104. * on top of the always visible background circle stroke, overlapping it. If set to false, the foreground pie/ring
  1105. * will be scaled down to fit into the background circle, not overlapping the latter's stroke.
  1106. * This is only advisable if the <code>strokeWidth</code> is small enough to leave free space inside the
  1107. * background circle. Also, this only makes any sense if the background circle's color differs from the
  1108. * foreground color (i.e. the <code>strokeColor</code> option is set) or if the foreground color is semi transparent.
  1109. * @property {RingAlign} ringAlign: defaults to $.fn.progressPie.RingAlign.OUTER, defines the alignment of a ring chart (only defined
  1110. * if the ringWidth option is set and &gt; 0) with the background circle (requires the strokeWidth option to be &gt; 0).
  1111. * @property {boolean} prepend - true for prepending the SVG graph to the selected element's content, false for appending. Defaults to true.
  1112. * @property {string} separator - String to be inserted between prepended or appended SVG and target element's content.
  1113. * If the target element is empty, i.e. there's no content to append or prepend the graph to
  1114. * (example: <code>&lt;span class="pie" data-percent="10"&gt;&lt;/span&gt;</code>), the separator and prepend options will be ignored
  1115. * and only the SVG will be inserted into the element (starting with V2.0.0)
  1116. * @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).
  1117. * @property {boolean} update - true will remove any SVG child from the selected target element before inserting a new image,
  1118. * false will only insert a new SVG if none exists yet. Defaults to false.
  1119. * @property {function} valueAdapter - Function takes a value (string or number) and returns a number in range (0..100),
  1120. * defaults to a function returning number values unchanged and applying parseFloat to string values. Note that this may
  1121. * parse percent numbers with decimal digits if the "dot" is used as decimal separator, while if any unknown character
  1122. * like a comma (european decimal separator) is found, the parsing stops and the rest of the string is ignored.
  1123. * I.e. a string like "23.5" is parsed as twenty-three and a half percent while "23,5" is parsed as exactly
  1124. * 23 percent, which usually should be exact enough if the pie chart is not very big.
  1125. * @property {string} valueInputEvents - space-separated list of event names, defaulting to <code>"change"</code>.
  1126. * This property is only used in conjunction with the <code>valueInput</code> option (and only using the
  1127. * <code>setupProgressPie()</code> function). The <code>valueInput</code> option binds a progress pie/ring
  1128. * to an input element and automatically registers an event handler listening on the input element to these events.
  1129. * If any of these events (by default only the <code>change</code> event) fires, the pie will automatically be
  1130. * updated to the current input's value (<code>.val()</code>).
  1131. * @property {boolean} ringEndsRounded - If setting a ringWidth, this flag controls if the ends of the ring are simply
  1132. * cut (false) or if half a circle is appended to each end of the ring section (true). Defaults to false.
  1133. * @property {number} sizeFactor - Defaults to 1. The "original" diameter for the pie chart as either auto-sized
  1134. * or specified by the <code>size</code> option, is multiplied with this factor to get the final diameter before
  1135. * drawing the pie chart.
  1136. * @property {number} scale - Defaults to 1. The already rendered SVG graphic is finally scaled by this factor.
  1137. * In difference to <code>sizeFactor</code> this does not simply change the diameter/radius of the chart, but scales
  1138. * all other aspects such as the <code>strokeWidth</code>, too.
  1139. * @property {number} defaultContentPluginBackgroundMarginFullSize - Defaults to 0. Sets the default value for a content plug-in's margin
  1140. * property if that plug-in uses the API's <code>getBackgroundRadius()</code> function, if the <code>contentPluginOptions</code> object does not
  1141. * specify a <code>margin</code> property and if a pie chart is drawn (i.e. the <code>ringWidth</code> option is not set) or if (on a ring chart)
  1142. * the <code>fullSize</code> property of the <code>contentPluginOption</code> is set to true.<br>
  1143. * The value of 0 causes a filled background to cover the whole pie.
  1144. * @property {number} defaultContentPluginBackgroundMarginInsideRing - Defaults to 1. Sets the default value for a content plug-in's margin
  1145. * property if that plug-in uses the API's <code>getBackgroundRadius()</code> function, if the <code>contentPluginOptions</code> object does not
  1146. * specify a <code>margin</code> property and does not set <code>fullSize</code> and if a ring is drawn (i.e. the <code>ringWidth</code> option <em>is</em> set).<br>
  1147. * 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,
  1148. * the content background would "touch" the ring.
  1149. * @property {string} cssClassBackgroundCircle - name of a CSS class assigned to the circle shape drawn as background
  1150. * behind the pie or ring segment. Defaults to <code>progresspie-background</code>.
  1151. * @property {string} cssClassForegroundPie - name of a CSS class assigned to the pie or ring segment (foreground).
  1152. * Defaults to <code>progresspie-foreground</code>.
  1153. * @property {string} cssClassOuter - If the <code>inner</code> option is used to draw a second value, this CSS
  1154. * class is assigned to the background circle as well as the foreground pie/ring segment of the outer/main value
  1155. * <em>in addition to</em> the respective cssClassBackgroundCircle or cssClassForegroundPie class.
  1156. * Defaults to <code>progresspie-outer</code>.
  1157. * @property {string} cssClassOuter - If the <code>inner</code> option is used to draw a second value, this CSS
  1158. * class as assigned to the background circle and the forground pie/ring segment of the inner value
  1159. * in addition to the respective cssClassBackgroundCircle or cssClassForegroundPie.
  1160. * Defaults to <code>progresspie-inner</code>. If the inner option contains a second inner option (third value),
  1161. * the background and foreground elements of the "inner inner" value get assigned this class with suffix "2",
  1162. * an "inner inner inner" value will be assigned this class with suffix "3" and so on.
  1163. */
  1164. $.fn.progressPie.defaults = {
  1165. mode: $.fn.progressPie.Mode.GREY,
  1166. margin: 0,
  1167. padding: 0,
  1168. strokeWidth: 2,
  1169. overlap: true,
  1170. ringAlign: $.fn.progressPie.RingAlign.OUTER,
  1171. prepend: true,
  1172. separator: "&nbsp;",
  1173. verticalAlign: "bottom",
  1174. update: false,
  1175. valueAdapter: function(value) {
  1176. if (typeof value === "string") {
  1177. return parseFloat(value);
  1178. } else if (typeof value === "number") {
  1179. return value;
  1180. } else {
  1181. return 0;
  1182. }
  1183. },
  1184. valueInputEvents: "change",
  1185. ringEndsRounded: false,
  1186. sizeFactor: 1,
  1187. scale: 1,
  1188. defaultContentPluginBackgroundMarginFullSize: 0,
  1189. defaultContentPluginBackgroundMarginInsideRing: 1,
  1190. cssClassBackgroundCircle: "progresspie-background",
  1191. cssClassForegroundPie: "progresspie-foreground",
  1192. cssClassOuter: "progresspie-outer",
  1193. cssClassInner: "progresspie-inner"
  1194. };
  1195. /**
  1196. * Default SMIL animation attributes for value transitions.
  1197. * keys and value syntax follow the SMIL language for SVG animation.
  1198. * Each property of this object will be turned into an attribute of the SMIL animation
  1199. * element, the property's key serving as attribute name, the property's value as attribute value.
  1200. * <p>If the plug-in is called with option <code>animate: true</code>, these options will be applied to
  1201. * the animation.<br>
  1202. * If the plug-in is called with option <code>animate: {options}</code>, the options enumerated by the
  1203. * user get added to these defaults. Each stated option will override the default option, while those
  1204. * properties of this defaults object that are not overridden by the user will be applied unchanged.<br>
  1205. * If the user, for example, adds the option <code>animate: {dur: "2s"}</code>, the default duration will
  1206. * be overriden, the animation will last 2 seconds, while the other animation properties (spline mode)
  1207. * will be applied exactly as defined in this defaults object.
  1208. * @memberOf jQuery.fn.progressPie
  1209. * @member defaultAnimationAttributes
  1210. * @property {string} dur - the duration of the animation (number with unit, e.g. "1s" or "700ms")
  1211. * @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}.
  1212. * @property {string} keySplines - see {@link http://www.w3.org/TR/SVG/animate.html#KeySplinesAttribute specification}
  1213. * @property {string} keyTimes - see {@link http://www.w3.org/TR/SVG/animate.html#KeyTimesAttribute specification}
  1214. */
  1215. $.fn.progressPie.defaultAnimationAttributes = {
  1216. dur: "1s",
  1217. calcMode: "spline",
  1218. keySplines: "0.23 1 0.32 1",
  1219. keyTimes: "0;1"
  1220. };
  1221. /**
  1222. * Default namespace for content plug-ins.
  1223. * If you write contentPlugin functions, it is recommended to add them as methods to this object
  1224. * (see bundled jquery-progresspiesvg-controlIcons.js for eample).
  1225. * Though you may use any function as a plugin (if it conforms to the plug-in interface),
  1226. * only functions within this default namespace may be specified by a string holding their function name
  1227. * in the <code>contentPlugin</code> option. Functions not in this namespace have to be referred to
  1228. * by a function reference (an expression evaluating to the very function object).
  1229. * <p>
  1230. * Any methods of this object documented in the bundled JSDoc are predefined content plug-ins, and member
  1231. * objects are used to define default options for those plug-in methods.
  1232. * @namespace contentPlugin
  1233. * @memberOf jQuery.fn.progressPie
  1234. */
  1235. $.fn.progressPie.contentPlugin = {};
  1236. /**
  1237. * If a user enables animated state transitions via the <code>animate</code> option, the plug-in
  1238. * stores the last drawn value in jQuery's data map associated with the target element. If the user
  1239. * later first updates the value to be shown and then calls the progressPie() plug-in again on the target
  1240. * element, it will not only find the new value, but also the previous value in said data, which it
  1241. * needs to calculate the transition.
  1242. * <p>This option defines the name of the data entry which will be added to the target DOM node
  1243. * to hold the lastly draw percent value.
  1244. * @memberOf jQuery.fn.progressPie
  1245. * @member prevValueDataName
  1246. */
  1247. $.fn.progressPie.prevValueDataName = "_progresspieSVG_prevValue";
  1248. /**
  1249. * Just like <code>prevValueDataName</code>, but used for double/multiple pies:
  1250. * The lastly drawn percent value for the <code>inner</code> pie will be stored in a data
  1251. * entry of this name.
  1252. * If even more than two values are drawn (by nested <code>inner</code> options), the value of
  1253. * a number will be appended to this name. So the lastly drawn value of the third pie (<code>inner.inner</code>)
  1254. * will be stored in a data entry named <code>prevInnerValueDataName+"2"</code>.
  1255. * @memberOf jQuery.fn.progressPie
  1256. * @member prevInnerValueDataName
  1257. */
  1258. $.fn.progressPie.prevInnerValueDataName = "_progresspieSVG_prevInnerValue";
  1259. }( jQuery ));