text: /**
   * Displays a value within the given interval as a gauge. For example:
   *
   *     @example
   *     Ext.create({
   *         xtype: 'panel',
   *         renderTo: document.body,
   *         width: 200,
   *         height: 200,
   *         layout: 'fit',
   *         items: {
   *             xtype: 'gauge',
   *             padding: 20,
   *             value: 55,
   *             minValue: 40,
   *             maxValue: 80
   *         }
   *     });
   *
   * It's also possible to use gauges to create loading indicators:
   *
   *     @example
   *     Ext.create({
   *         xtype: 'panel',
   *         renderTo: document.body,
   *         width: 200,
   *         height: 200,
   *         layout: 'fit',
   *         items: {
   *             xtype: 'gauge',
   *             padding: 20,
   *             trackStart: 0,
   *             trackLength: 360,
   *             value: 20,
   *             valueStyle: {
   *                 round: true
   *             },
   *             textTpl: 'Loading...',
   *             animation: {
   *                 easing: 'linear',
   *                 duration: 100000
   *             }
   *         }
   *     }).items.first().setAngleOffset(360 * 100);
   */
  Ext.define('Ext.ux.Gauge', {
      extend: 'Ext.Gadget',
      xtype: 'gauge',
  
      requires: [
          'Ext.util.Region'
      ],
  
      config: {
          /**
           * @cfg {Number/String} padding Gauge sector padding in pixels or percent of
           * width/height, whichever is smaller.
           */
          padding: 10,
  
          /**
           * @cfg {Number} trackStart
           * The angle in the [0, 360) interval at which the gauge's track sector starts.
           * E.g. 0 for 3 o-clock, 90 for 6 o-clock, 180 for 9 o-clock, 270 for noon.
           */
          trackStart: 135,
  
          /**
           * @cfg {Number} trackLength
           * The angle in the (0, 360] interval to add to the {@link #trackStart} angle
           * to determine the angle at which the track ends.
           */
          trackLength: 270,
  
          /**
           * @cfg {Number} angleOffset
           * The angle at which the {@link #minValue} starts in case of a circular gauge.
           */
          angleOffset: 0,
  
          /**
           * @cfg {Number} minValue The minimum value that the gauge can represent.
           */
          minValue: 0,
  
          /**
           * @cfg {Number} maxValue The maximum value that the gauge can represent.
           */
          maxValue: 100,
  
          /**
           * @cfg {Number} value The current value of the gauge.
           */
          value: 50,
  
          /**
           * @cfg {Boolean} [clockwise=true]
           * `true` - {@link #cfg!value} increments in a clockwise fashion
           * `false` - {@link #cfg!value} increments in an anticlockwise fashion
           */
          clockwise: true,
  
          /**
           * @cfg {Ext.XTemplate} textTpl The template for the text in the center of the gauge.
           * The available data values are:
           * - `value` - The {@link #cfg!value} of the gauge.
           * - `percent` - The value as a percentage between 0 and 100.
           * - `minValue` - The value of the {@link #cfg!minValue} config.
           * - `maxValue` - The value of the {@link #cfg!maxValue} config.
           * - `delta` - The delta between the {@link #cfg!minValue} and {@link #cfg!maxValue}.
           */
          textTpl: ['<tpl>{value:number("0.00")}%</tpl>'],
  
          /**
           * @cfg {String} [textAlign='c-c']
           * If the gauge has a donut hole, the text will be centered inside it.
           * Otherwise, the text will be centered in the middle of the gauge's
           * bounding box. This config allows to alter the position of the text
           * in the latter case. See the docs for the `align` option to the
           * {@Ext.util.Region#alignTo} method for possible ways of alignment
           * of the text to the guage's bounding box.
           */
          textAlign: 'c-c',
  
          /**
           * @cfg {Object} trackStyle Track sector styles.
           * @cfg {String/Object[]} trackStyle.fill Track sector fill color. Defaults to CSS value.
           * It's also possible to have a linear gradient fill that starts at the top-left corner
           * of the gauge and ends at its bottom-right corner, by providing an array of color stop
           * objects. For example:
           *
           *     trackStyle: {
           *         fill: [{
           *             offset: 0,
           *             color: 'green',
           *             opacity: 0.8
           *         }, {
           *             offset: 1,
           *             color: 'gold'
           *         }]
           *     }
           *
           * @cfg {Number} trackStyle.fillOpacity Track sector fill opacity. Defaults to CSS value.
           * @cfg {String} trackStyle.stroke Track sector stroke color. Defaults to CSS value.
           * @cfg {Number} trackStyle.strokeOpacity Track sector stroke opacity. Defaults to CSS value.
           * @cfg {Number} trackStyle.strokeWidth Track sector stroke width. Defaults to CSS value.
           * @cfg {Number/String} [trackStyle.outerRadius='100%'] The outer radius of the track sector.
           * For example:
           *
           *     outerRadius: '90%',      // 90% of the maximum radius
           *     outerRadius: 100,        // radius of 100 pixels
           *     outerRadius: '70% + 5',  // 70% of the maximum radius plus 5 pixels
           *     outerRadius: '80% - 10', // 80% of the maximum radius minus 10 pixels
           *
           * @cfg {Number/String} [trackStyle.innerRadius='50%'] The inner radius of the track sector.
           * See the `trackStyle.outerRadius` config documentation for more information.
           * @cfg {Boolean} [trackStyle.round=false] Whether to round the track sector edges or not.
           */
          trackStyle: {
              outerRadius: '100%',
              innerRadius: '100% - 20',
              round: false
          },
  
          /**
           * @cfg {Object} valueStyle Value sector styles.
           * @cfg {String/Object[]} valueStyle.fill Value sector fill color. Defaults to CSS value.
           * See the `trackStyle.fill` config documentation for more information.
           * @cfg {Number} valueStyle.fillOpacity Value sector fill opacity. Defaults to CSS value.
           * @cfg {String} valueStyle.stroke Value sector stroke color. Defaults to CSS value.
           * @cfg {Number} valueStyle.strokeOpacity Value sector stroke opacity. Defaults to CSS value.
           * @cfg {Number} valueStyle.strokeWidth Value sector stroke width. Defaults to CSS value.
           * @cfg {Number/String} [valueStyle.outerRadius='100% - 4'] The outer radius of the value sector.
           * See the `trackStyle.outerRadius` config documentation for more information.
           * @cfg {Number/String} [valueStyle.innerRadius='50% + 4'] The inner radius of the value sector.
           * See the `trackStyle.outerRadius` config documentation for more information.
           * @cfg {Boolean} [valueStyle.round=false] Whether to round the value sector edges or not.
           */
          valueStyle: {
              outerRadius: '100% - 2',
              innerRadius: '100% - 18',
              round: false
          },
  
          /**
           * @cfg {Object/Boolean} [animation=true]
           * The animation applied to the gauge on changes to the {@link #value}
           * and the {@link #angleOffset} configs. Defaults to 1 second animation
           * with the  'out' easing.
           * @cfg {Number} animation.duration The duraction of the animation.
           * @cfg {String} animation.easing The easing function to use for the animation.
           * Possible values are:
           * - `linear` - no easing, no acceleration
           * - `in` - accelerating from zero velocity
           * - `out` - (default) decelerating to zero velocity
           * - `inOut` - acceleration until halfway, then deceleration
           */
          animation: true
      },
  
      baseCls: Ext.baseCSSPrefix + 'gauge',
  
      template: [{
          reference: 'bodyElement',
          children: [{
              reference: 'textElement',
              cls: Ext.baseCSSPrefix + 'gauge-text'
          }]
      }],
  
      defaultBindProperty: 'value',
  
      pathAttributes: {
          // The properties in the `trackStyle` and `valueStyle` configs
          // that are path attributes.
          fill: true,
          fillOpacity: true,
          stroke: true,
          strokeOpacity: true,
          strokeWidth: true
      },
  
      easings: {
          linear: Ext.identityFn,
          // cubic easings
          'in': function (t) { return t*t*t; },
          out: function (t) { return (--t)*t*t+1; },
          inOut: function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1; }
      },
  
      resizeDelay: 0,   // in milliseconds
      resizeTimerId: 0,
      size: null,       // cached size
      svgNS: 'http://www.w3.org/2000/svg',
      svg: null,        // SVG document
      defs: null,       // the `defs` section of the SVG document
      trackArc: null,
      valueArc: null,
      trackGradient: null,
      valueGradient: null,
      fx: null,         // either the `value` or the `angleOffset` animation
      fxValue: 0,       // the actual value rendered/animated
      fxAngleOffset: 0,
  
      constructor: function (config) {
          var me = this;
  
          me.fitSectorInRectCache = {
              startAngle: null,
              lengthAngle: null,
              minX: null,
              maxX: null,
              minY: null,
              maxY: null
          };
  
          me.interpolator = me.createInterpolator();
          me.callParent([config]);
  
          me.el.on('resize', 'onElementResize', me);
      },
  
      doDestroy: function () {
          var me = this;
  
          Ext.undefer(me.resizeTimerId);
          me.el.un('resize', 'onElementResize', me);
          me.stopAnimation();
          me.svg = Ext.destroy(me.svg);
          
          me.callParent();
      },
  
      // <if classic>
      afterComponentLayout: function(width, height, oldWidth, oldHeight) {
          this.callParent([width, height, oldWidth, oldHeight]);
  
          if (Ext.isIE9) {
              this.handleResize();
          }
      },
      // </if>
  
      onElementResize: function (element, size) {
          this.handleResize(size);
      },
  
      handleResize: function (size, instantly) {
          var me = this,
              el = me.element;
  
          if (!(el && (size = size || el.getSize()) && size.width && size.height)) {
              return;
          }
  
          me.resizeTimerId = Ext.undefer(me.resizeTimerId);
  
          if (!instantly && me.resizeDelay) {
              me.resizeTimerId = Ext.defer(me.handleResize, me.resizeDelay, me, [size, true]);
              return;
          }
  
          me.size = size;
          me.resizeHandler(size);
      },
  
      updateMinValue: function (minValue) {
          var me = this;
  
          me.interpolator.setDomain(minValue, me.getMaxValue());
          if (!me.isConfiguring) {
              me.render();
          }
      },
  
      updateMaxValue: function (maxValue) {
          var me = this;
  
          me.interpolator.setDomain(me.getMinValue(), maxValue);
          if (!me.isConfiguring) {
              me.render();
          }
      },
  
      updateAngleOffset: function (angleOffset, oldAngleOffset) {
          var me = this,
              animation = me.getAnimation();
  
          me.fxAngleOffset = angleOffset;
  
          if (!me.isConfiguring) {
              if (animation.duration) {
                  me.animate(oldAngleOffset, angleOffset, animation.duration, me.easings[animation.easing], function (angleOffset) {
                      me.fxAngleOffset = angleOffset;
                      me.render();
                  });
              } else {
                  me.render();
              }
          }
      },
  
      //<debug>
      applyTrackStart: function (trackStart) {
          if (trackStart < 0 || trackStart >= 360) {
              Ext.raise("'trackStart' should be within [0, 360).");
          }
          return trackStart;
      },
  
      applyTrackLength: function (trackLength) {
          if (trackLength <= 0 || trackLength > 360) {
              Ext.raise("'trackLength' should be within (0, 360].");
          }
          return trackLength;
      },
      //</debug>
  
      updateTrackStart: function (trackStart) {
          var me = this;
  
          if (!me.isConfiguring) {
              me.render();
          }
      },
  
      updateTrackLength: function (trackLength) {
          var me = this;
  
          me.interpolator.setRange(0, trackLength);
          if (!me.isConfiguring) {
              me.render();
          }
      },
  
      applyPadding: function (padding) {
          if (typeof padding === 'string') {
              var ratio = parseFloat(padding) / 100;
              return function (x) {
                  return x * ratio;
              };
          }
          return function () {
              return padding;
          };
      },
  
      updatePadding: function () {
          if (!this.isConfiguring) {
              this.render();
          }
      },
  
      applyValue: function (value) {
          var minValue = this.getMinValue(),
              maxValue = this.getMaxValue();
  
          return Math.min(Math.max(value, minValue), maxValue);
      },
  
      updateValue: function (value, oldValue) {
          var me = this,
              animation = me.getAnimation();
  
          me.fxValue = value;
  
          if (!me.isConfiguring) {
              me.writeText();
              if (animation.duration) {
                  me.animate(oldValue, value, animation.duration, me.easings[animation.easing], function (value) {
                      me.fxValue = value;
                      me.render();
                  });
              } else {
                  me.render();
              }
          }
      },
  
      applyTextTpl: function (textTpl) {
          if (textTpl && !textTpl.isTemplate) {
              textTpl = new Ext.XTemplate(textTpl);
          }
          return textTpl;
      },
  
      updateTextTpl: function () {
          this.writeText();
          if (!this.isConfiguring) {
              this.centerText(); // text will be centered on first size
          }
      },
  
      writeText: function (options) {
          var me = this,
              value = me.getValue(),
              minValue = me.getMinValue(),
              maxValue = me.getMaxValue(),
              delta = maxValue - minValue,
              textTpl = me.getTextTpl();
  
          textTpl.overwrite(me.textElement, {
              value: value,
              percent: (value - minValue) / delta * 100,
              minValue: minValue,
              maxValue: maxValue,
              delta: delta
          });
      },
  
      centerText: function (cx, cy, sectorRegion, innerRadius, outerRadius) {
          var textElement = this.textElement,
              textAlign = this.getTextAlign(),
              alignedRegion, textBox;
  
          if (Ext.Number.isEqual(innerRadius, 0, 0.1) || sectorRegion.isOutOfBound({x: cx, y: cy})) {
              alignedRegion = textElement.getRegion().alignTo({
                  align: textAlign, // align text region's center to sector region's center
                  target: sectorRegion
              });
              textElement.setLeft(alignedRegion.left);
              textElement.setTop(alignedRegion.top);
          } else {
              textBox = textElement.getBox();
              textElement.setLeft(cx - textBox.width / 2);
              textElement.setTop(cy - textBox.height / 2);
          }
      },
  
      camelCaseRe: /([a-z])([A-Z])/g,
  
      /**
       * @private
       */
      camelToHyphen: function (name) {
          return name.replace(this.camelCaseRe, '$1-$2').toLowerCase();
      },
  
      applyTrackStyle: function (trackStyle) {
          var me = this,
              trackGradient;
  
          trackStyle.innerRadius = me.getRadiusFn(trackStyle.innerRadius);
          trackStyle.outerRadius = me.getRadiusFn(trackStyle.outerRadius);
  
          if (Ext.isArray(trackStyle.fill)) {
              trackGradient = me.getTrackGradient();
              me.setGradientStops(trackGradient, trackStyle.fill);
              trackStyle.fill = 'url(#' + trackGradient.getAttribute('id') + ')';
          }
  
          return trackStyle;
      },
  
      updateTrackStyle: function (trackStyle) {
          var me = this,
              trackArc = Ext.fly(me.getTrackArc()),
              name;
  
          for (name in trackStyle) {
              if (name in me.pathAttributes) {
                  trackArc.setStyle(me.camelToHyphen(name), trackStyle[name]);
              }
          }
      },
  
      applyValueStyle: function (valueStyle) {
          var me = this,
              valueGradient;
  
          valueStyle.innerRadius = me.getRadiusFn(valueStyle.innerRadius);
          valueStyle.outerRadius = me.getRadiusFn(valueStyle.outerRadius);
  
          if (Ext.isArray(valueStyle.fill)) {
              valueGradient = me.getValueGradient();
              me.setGradientStops(valueGradient, valueStyle.fill);
              valueStyle.fill = 'url(#' + valueGradient.getAttribute('id') + ')';
          }
  
          return valueStyle;
      },
  
      updateValueStyle: function (valueStyle) {
          var me = this,
              valueArc = Ext.fly(me.getValueArc()),
              name;
  
          for (name in valueStyle) {
              if (name in me.pathAttributes) {
                  valueArc.setStyle(me.camelToHyphen(name), valueStyle[name]);
              }
          }
      },
  
      /**
       * @private
       */
      getRadiusFn: function (radius) {
          var result, pos, ratio,
              increment = 0;
  
          if (Ext.isNumber(radius)) {
              result = function () {
                  return radius;
              };
          } else if (Ext.isString(radius)) {
              radius = radius.replace(/ /g, '');
              ratio = parseFloat(radius) / 100;
              pos = radius.search('%'); // E.g. '100% - 4'
              if (pos < radius.length - 1) {
                  increment = parseFloat(radius.substr(pos + 1));
              }
              result = function (radius) {
                  return radius * ratio + increment;
              };
              result.ratio = ratio;
          }
  
          return result;
      },
  
      getSvg: function () {
          var me = this,
              svg = me.svg;
  
          if (!svg) {
              svg = me.svg = Ext.get(document.createElementNS(me.svgNS, 'svg'));
              me.bodyElement.append(svg);
          }
  
          return svg;
      },
  
      getTrackArc: function () {
          var me = this,
              trackArc = me.trackArc;
          
          if (!trackArc) {
              trackArc = me.trackArc = document.createElementNS(me.svgNS, 'path');
              me.getSvg().append(trackArc, true);
              // Note: Ext.dom.Element.addCls doesn't work on SVG elements,
              // as it simply assigns a class string to el.dom.className,
              // which in case of SVG is no simple string:
              // SVGAnimatedString {baseVal: "x-gauge-track", animVal: "x-gauge-track"}
              trackArc.setAttribute('class', Ext.baseCSSPrefix + 'gauge-track');
          }
          
          return trackArc;
      },
  
      getValueArc: function () {
          var me = this,
              valueArc = me.valueArc;
  
          me.getTrackArc(); // make sure the track arc is created first for proper draw order
          if (!valueArc) {
              valueArc = me.valueArc = document.createElementNS(me.svgNS, 'path');
              me.getSvg().append(valueArc, true);
              valueArc.setAttribute('class', Ext.baseCSSPrefix + 'gauge-value');
          }
  
          return valueArc;
      },
  
      getDefs: function () {
          var me = this,
              defs = me.defs;
  
          if (!defs) {
              defs = me.defs = document.createElementNS(me.svgNS, 'defs');
              me.getSvg().append(defs);
          }
  
          return defs;
      },
  
      /**
       * @private
       */
      setGradientSize: function (gradient, x1, y1, x2, y2) {
          gradient.setAttribute('x1', x1);
          gradient.setAttribute('y1', y1);
          gradient.setAttribute('x2', x2);
          gradient.setAttribute('y2', y2);
      },
  
      /**
       * @private
       */
      resizeGradients: function (size) {
          var me = this,
              trackGradient = me.getTrackGradient(),
              valueGradient = me.getValueGradient(),
              x1 = 0,
              y1 = size.height / 2,
              x2 = size.width,
              y2 = size.height / 2;
  
          me.setGradientSize(trackGradient, x1, y1, x2, y2);
          me.setGradientSize(valueGradient, x1, y1, x2, y2);
      },
  
      /**
       * @private
       */
      setGradientStops: function (gradient, stops) {
          var ln = stops.length,
              i, stopCfg, stopEl;
  
          while (gradient.firstChild) {
              gradient.removeChild(gradient.firstChild);
          }
          for (i = 0; i < ln; i++) {
              stopCfg = stops[i];
              stopEl = document.createElementNS(this.svgNS, 'stop');
              gradient.appendChild(stopEl);
              stopEl.setAttribute('offset', stopCfg.offset);
              stopEl.setAttribute('stop-color', stopCfg.color);
              ('opacity' in stopCfg) && stopEl.setAttribute('stop-opacity', stopCfg.opacity);
          }
      },
  
      getTrackGradient: function () {
          var me = this,
              trackGradient = me.trackGradient;
  
          if (!trackGradient) {
              trackGradient = me.trackGradient = document.createElementNS(me.svgNS, 'linearGradient');
              // Using absolute values for x1, y1, x2, y2 attributes.
              trackGradient.setAttribute('gradientUnits', 'userSpaceOnUse');
              me.getDefs().appendChild(trackGradient);
              Ext.get(trackGradient); // assign unique ID
          }
  
          return trackGradient;
      },
  
      getValueGradient: function () {
          var me = this,
              valueGradient = me.valueGradient;
  
          if (!valueGradient) {
              valueGradient = me.valueGradient = document.createElementNS(me.svgNS, 'linearGradient');
              // Using absolute values for x1, y1, x2, y2 attributes.
              valueGradient.setAttribute('gradientUnits', 'userSpaceOnUse');
              me.getDefs().appendChild(valueGradient);
              Ext.get(valueGradient); // assign unique ID
          }
  
          return valueGradient;
      },
  
      getArcPoint: function (centerX, centerY, radius, degrees) {
          var radians = degrees / 180 * Math.PI;
  
          return [
              centerX + radius * Math.cos(radians),
              centerY + radius * Math.sin(radians)
          ];
      },
  
      isCircle: function (startAngle, endAngle) {
          return Ext.Number.isEqual(Math.abs(endAngle - startAngle), 360, .001);
      },
  
      getArcPath: function (centerX, centerY, innerRadius, outerRadius, startAngle, endAngle, round) {
          var me = this,
              isCircle = me.isCircle(startAngle, endAngle),
              // It's not possible to draw a circle using arcs.
              endAngle = endAngle - 0.01,
              innerStartPoint = me.getArcPoint(centerX, centerY, innerRadius, startAngle),
              innerEndPoint = me.getArcPoint(centerX, centerY, innerRadius, endAngle),
              outerStartPoint = me.getArcPoint(centerX, centerY, outerRadius, startAngle),
              outerEndPoint = me.getArcPoint(centerX, centerY, outerRadius, endAngle),
              large = endAngle - startAngle <= 180 ? 0 : 1,
              path = [
                  'M', innerStartPoint[0], innerStartPoint[1],
                  'A', innerRadius, innerRadius, 0, large, 1, innerEndPoint[0], innerEndPoint[1]
              ],
              capRadius = (outerRadius - innerRadius) / 2;
  
          if (isCircle) {
              path.push('M', outerEndPoint[0], outerEndPoint[1]);
          } else {
              if (round) {
                  path.push('A', capRadius, capRadius, 0, 0, 0, outerEndPoint[0], outerEndPoint[1]);
              } else {
                  path.push('L', outerEndPoint[0], outerEndPoint[1]);
              }
          }
  
          path.push('A', outerRadius, outerRadius, 0, large, 0, outerStartPoint[0], outerStartPoint[1]);
  
          if (round && !isCircle) {
              path.push('A', capRadius, capRadius, 0, 0, 0, innerStartPoint[0], innerStartPoint[1]);
          }
          path.push('Z');
  
          return path.join(' ');
      },
  
      resizeHandler: function (size) {
          var me = this,
              svg = me.getSvg();
  
          svg.setSize(size);
          me.resizeGradients(size);
          me.render();
      },
  
      /**
       * @private
       */
      createInterpolator: function (rangeCheck) {
          var domainStart = 0,
              domainDelta = 1,
              rangeStart = 0,
              rangeEnd = 1;
  
          var interpolator = function (x, invert) {
              var t = 0;
  
              if (domainDelta) {
                  t = (x - domainStart) / domainDelta;
                  if (rangeCheck) {
                      t = Math.max(0, t);
                      t = Math.min(1, t);
                  }
                  if (invert) {
                      t = 1 - t;
                  }
              }
  
              return (1 - t) * rangeStart + t * rangeEnd;
          };
          interpolator.setDomain = function (a, b) {
              domainStart = a;
              domainDelta = b - a;
              return this;
          };
          interpolator.setRange = function (a, b) {
              rangeStart = a;
              rangeEnd = b;
              return this;
          };
          interpolator.getDomain = function () {
              return [domainStart, domainStart + domainDelta];
          };
          interpolator.getRange = function () {
              return [rangeStart, rangeEnd];
          };
  
          return interpolator;
      },
  
      applyAnimation: function (animation) {
          if (true === animation) {
              animation = {};
          } else if (false === animation) {
              animation = {
                  duration: 0
              };
          }
          if (!('duration' in animation)) {
              animation.duration = 1000;
          }
          if (!(animation.easing in this.easings)) {
              animation.easing = 'out';
          }
          return animation;
      },
  
      updateAnimation: function () {
          this.stopAnimation();
      },
  
      /**
       * @private
       */
      animate: function (from, to, duration, easing, fn, scope) {
          var me = this,
              start = Ext.now(),
              interpolator = me.createInterpolator().setRange(from, to);
  
          function frame() {
              var now = Ext.AnimationQueue.frameStartTime,
                  t = Math.min(now - start, duration) / duration,
                  value = interpolator(easing(t));
  
              if (scope) {
                  if (typeof fn === 'string') {
                      scope[fn].call(scope, value);
                  } else {
                      fn.call(scope, value);
                  }
              } else {
                  fn(value);
              }
  
              if (t >= 1) {
                  Ext.AnimationQueue.stop(frame, scope);
                  me.fx = null;
              }
          }
          me.stopAnimation();
          Ext.AnimationQueue.start(frame, scope);
          me.fx = {
              frame: frame,
              scope: scope
          };
      },
  
      /**
       * Stops the current {@link #value} or {@link #angleOffset} animation.
       */
      stopAnimation: function () {
          var me = this;
  
          if (me.fx) {
              Ext.AnimationQueue.stop(me.fx.frame, me.fx.scope);
              me.fx = null;
          }
      },
  
      unitCircleExtrema: {
          0: [1, 0],
          90: [0, 1],
          180: [-1, 0],
          270: [0, -1],
          360: [1, 0],
          450: [0, 1],
          540: [-1, 0],
          630: [0, -1]
      },
  
      /**
       * @private
       */
      getUnitSectorExtrema: function (startAngle, lengthAngle) {
          var extrema = this.unitCircleExtrema,
              points = [],
              angle;
  
          for (angle in extrema) {
              if (angle > startAngle && angle < startAngle + lengthAngle) {
                  points.push(extrema[angle]);
              }
          }
  
          return points;
      },
  
      /**
       * @private
       * Given a rect with a known width and height, find the maximum radius of the donut
       * sector that can fit into it, as well as the center point of such a sector.
       * The end and start angles of the sector are also known, as well as the relationship
       * between the inner and outer radii.
       */
      fitSectorInRect: function (width, height, startAngle, lengthAngle, ratio) {
          if (Ext.Number.isEqual(lengthAngle, 360, 0.001)) {
              return {
                  cx: width / 2,
                  cy: height / 2,
                  radius: Math.min(width, height) / 2,
                  region: new Ext.util.Region(0, width, height, 0)
              };
          }
  
          var me = this,
              points, xx, yy, minX, maxX, minY, maxY,
              cache = me.fitSectorInRectCache,
              sameAngles = cache.startAngle === startAngle
                        && cache.lengthAngle === lengthAngle;
  
          if (sameAngles) {
              minX = cache.minX;
              maxX = cache.maxX;
              minY = cache.minY;
              maxY = cache.maxY;
          } else {
              points = me.getUnitSectorExtrema(startAngle, lengthAngle).concat([
                  me.getArcPoint(0, 0, 1, startAngle),                  // start angle outer radius point
                  me.getArcPoint(0, 0, ratio, startAngle),              // start angle inner radius point
                  me.getArcPoint(0, 0, 1, startAngle + lengthAngle),    // end angle outer radius point
                  me.getArcPoint(0, 0, ratio, startAngle + lengthAngle) // end angle inner radius point
              ]);
              xx = points.map(function (point) { return point[0]; });
              yy = points.map(function (point) { return point[1]; });
              // The bounding box of a unit sector with the given properties.
              minX = Math.min.apply(null, xx);
              maxX = Math.max.apply(null, xx);
              minY = Math.min.apply(null, yy);
              maxY = Math.max.apply(null, yy);
  
              cache.startAngle = startAngle;
              cache.lengthAngle = lengthAngle;
              cache.minX = minX;
              cache.maxX = maxX;
              cache.minY = minY;
              cache.maxY = maxY;
          }
  
          var sectorWidth = maxX - minX,
              sectorHeight = maxY - minY,
              scaleX = width / sectorWidth,
              scaleY = height / sectorHeight,
              scale = Math.min(scaleX, scaleY),
              // Region constructor takes: top, right, bottom, left.
              sectorRegion = new Ext.util.Region(minY * scale, maxX * scale, maxY * scale, minX * scale),
              rectRegion = new Ext.util.Region(0, width, height, 0),
              alignedRegion = sectorRegion.alignTo({
                  align: 'c-c', // align sector region's center to rect region's center
                  target: rectRegion
              }),
              dx = alignedRegion.left - minX * scale,
              dy = alignedRegion.top - minY * scale;
  
          return {
              cx: dx,
              cy: dy,
              radius: scale,
              region: alignedRegion
          };
      },
  
      /**
       * @private
       */
      fitSectorInPaddedRect: function (width, height, padding, startAngle, lengthAngle, ratio) {
          var result = this.fitSectorInRect(
              width - padding * 2,
              height - padding * 2,
              startAngle, lengthAngle, ratio
          );
  
          result.cx += padding;
          result.cy += padding;
          result.region.translateBy(padding, padding);
  
          return result;
      },
  
      /**
       * @private
       */
      normalizeAngle: function (angle) {
          return (angle % 360 + 360) % 360;
      },
  
      render: function () {
          if (!this.size) {
              return;
          }
  
          var me = this,
              trackArc = me.getTrackArc(),
              valueArc = me.getValueArc(),
              clockwise = me.getClockwise(),
              value = me.fxValue,
              angleOffset = me.fxAngleOffset,
              trackLength = me.getTrackLength(),
              width = me.size.width,
              height = me.size.height,
              paddingFn = me.getPadding(),
              padding = paddingFn(Math.min(width, height)),
              trackStart = me.normalizeAngle(me.getTrackStart() + angleOffset), // in the range of [0, 360)
              trackEnd = trackStart + trackLength,                              // in the range of (0, 720)
              valueLength = me.interpolator(value),
              trackStyle = me.getTrackStyle(),
              valueStyle = me.getValueStyle(),
              sector = me.fitSectorInPaddedRect(width, height, padding, trackStart, trackLength, trackStyle.innerRadius.ratio),
              cx = sector.cx,
              cy = sector.cy,
              radius = sector.radius,
              trackInnerRadius = Math.max(0, trackStyle.innerRadius(radius)),
              trackOuterRadius = Math.max(0, trackStyle.outerRadius(radius)),
              valueInnerRadius = Math.max(0, valueStyle.innerRadius(radius)),
              valueOuterRadius = Math.max(0, valueStyle.outerRadius(radius)),
              trackPath = me.getArcPath(cx, cy, trackInnerRadius, trackOuterRadius, trackStart, trackEnd, trackStyle.round),
              valuePath = me.getArcPath(cx, cy, valueInnerRadius, valueOuterRadius,
                  clockwise ? trackStart : trackEnd - valueLength,
                  clockwise ? trackStart + valueLength : trackEnd,
                  valueStyle.round
              );
  
          me.centerText(cx, cy, sector.region, trackInnerRadius, trackOuterRadius);
  
          trackArc.setAttribute('d', trackPath);
          valueArc.setAttribute('d', valuePath);
      }
  
  });
  