extjs-gpl
Version:
GPL licensed version of Sencha Ext JS
1,430 lines (1,426 loc) • 500 kB
JavaScript
/**
* 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: {
baseCls: Ext.baseCSSPrefix + 'gauge',
/**
* @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} start The current value of the gauge.
*/
value: 50,
/**
* @cfg {Boolean} [clockwise=true]
* `true` - {@link #value} increments in a clockwise fashion
* `false` - {@link #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 #value} of the gauge.
* - `percent` - The value as a percentage between 0 and 100.
* - `minValue` - The value of the {@link #minValue} config.
* - `maxValue` - The value of the {@link #maxValue} config.
* - `delta` - The delta between the {@link #minValue} and {@link #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
},
template: [
{
reference: 'innerElement',
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 < 0.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.on('resize', 'onElementResize', me);
},
doDestroy: function() {
var me = this;
me.un('resize', 'onElementResize', me);
me.stopAnimation();
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;
}
clearTimeout(me.resizeTimerId);
if (instantly || me.resizeDelay) {
me.resizeTimerId = 0;
} else {
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.innerElement.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);
// 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);
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, 0.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);
}
});
/**
* This is a base class for more advanced "simlets" (simulated servers). A simlet is asked
* to provide a response given a {@link Ext.ux.ajax.SimXhr} instance.
*/
Ext.define('Ext.ux.ajax.Simlet', function() {
var urlRegex = /([^?#]*)(#.*)?$/,
dateRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/,
intRegex = /^[+-]?\d+$/,
floatRegex = /^[+-]?\d+\.\d+$/;
function parseParamValue(value) {
var m;
if (Ext.isDefined(value)) {
value = decodeURIComponent(value);
if (intRegex.test(value)) {
value = parseInt(value, 10);
} else if (floatRegex.test(value)) {
value = parseFloat(value);
} else if (!!(m = dateRegex.exec(value))) {
value = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]));
}
}
return value;
}
return {
alias: 'simlet.basic',
isSimlet: true,
responseProps: [
'responseText',
'responseXML',
'status',
'statusText',
'responseHeaders'
],
/**
* @cfg {String/Function} responseText
*/
/**
* @cfg {String/Function} responseXML
*/
/**
* @cfg {Object/Function} responseHeaders
*/
/**
* @cfg {Number/Function} status
*/
status: 200,
/**
* @cfg {String/Function} statusText
*/
statusText: 'OK',
constructor: function(config) {
Ext.apply(this, config);
},
doGet: function(ctx) {
return this.handleRequest(ctx);
},
doPost: function(ctx) {
return this.handleRequest(ctx);
},
doRedirect: function(ctx) {
return false;
},
doDelete: function(ctx) {
var me = this,
xhr = ctx.xhr,
records = xhr.options.records;
me.removeFromData(ctx, records);
},
/**
* Performs the action requested by the given XHR and returns an object to be applied
* on to the XHR (containing `status`, `responseText`, etc.). For the most part,
* this is delegated to `doMethod` methods on this class, such as `doGet`.
*
* @param {Ext.ux.ajax.SimXhr} xhr The simulated XMLHttpRequest instance.
* @return {Object} The response properties to add to the XMLHttpRequest.
*/
exec: function(xhr) {
var me = this,
ret = {},
method = 'do' + Ext.String.capitalize(xhr.method.toLowerCase()),
// doGet
fn = me[method];
if (fn) {
ret = fn.call(me, me.getCtx(xhr.method, xhr.url, xhr));
} else {
ret = {
status: 405,
statusText: 'Method Not Allowed'
};
}
return ret;
},
getCtx: function(method, url, xhr) {
return {
method: method,
params: this.parseQueryString(url),
url: url,
xhr: xhr
};
},
handleRequest: function(ctx) {
var me = this,
ret = {},
val;
Ext.Array.forEach(me.responseProps, function(prop) {
if (prop in me) {
val = me[prop];
if (Ext.isFunction(val)) {
val = val.call(me, ctx);
}
ret[prop] = val;
}
});
return ret;
},
openRequest: function(method, url, options, async) {
var ctx = this.getCtx(method, url),
redirect = this.doRedirect(ctx),
xhr;
if (options.action === 'destroy') {
method = 'delete';
}
if (redirect) {
xhr = redirect;
} else {
xhr = new Ext.ux.ajax.SimXhr({
mgr: this.manager,
simlet: this,
options: options
});
xhr.open(method, url, async);
}
return xhr;
},
parseQueryString: function(str) {
var m = urlRegex.exec(str),
ret = {},
key, value, i, n;
if (m && m[1]) {
var pair,
parts = m[1].split('&');
for (i = 0 , n = parts.length; i < n; ++i) {
if ((pair = parts[i].split('='))[0]) {
key = decodeURIComponent(pair.shift());
value = parseParamValue((pair.length > 1) ? pair.join('=') : pair[0]);
if (!(key in ret)) {
ret[key] = value;
} else if (Ext.isArray(ret[key])) {
ret[key].push(value);
} else {
ret[key] = [
ret[key],
value
];
}
}
}
}
return ret;
},
redirect: function(method, url, params) {
switch (arguments.length) {
case 2:
if (typeof url == 'string') {
break;
};
params = url;
// fall...
case 1:
url = method;
method = 'GET';
break;
}
if (params) {
url = Ext.urlAppend(url, Ext.Object.toQueryString(params));
}
return this.manager.openRequest(method, url);
},
removeFromData: function(ctx, records) {
var me = this,
data = me.getData(ctx),
model = (ctx.xhr.options.proxy && ctx.xhr.options.proxy.getModel()) || {},
idProperty = model.idProperty || 'id';
Ext.each(records, function(record) {
var id = record.get(idProperty);
for (var i = data.length; i-- > 0; ) {
if (data[i][idProperty] === id) {
me.deleteRecord(i);
break;
}
}
});
}
};
}());
/**
* This base class is used to handle data preparation (e.g., sorting, filtering and
* group summary).
*/
Ext.define('Ext.ux.ajax.DataSimlet', function() {
function makeSortFn(def, cmp) {
var order = def.direction,
sign = (order && order.toUpperCase() === 'DESC') ? -1 : 1;
return function(leftRec, rightRec) {
var lhs = leftRec[def.property],
rhs = rightRec[def.property],
c = (lhs < rhs) ? -1 : ((rhs < lhs) ? 1 : 0);
if (c || !cmp) {
return c * sign;
}
return cmp(leftRec, rightRec);
};
}
function makeSortFns(defs, cmp) {
for (var sortFn = cmp,
i = defs && defs.length; i; ) {
sortFn = makeSortFn(defs[--i], sortFn);
}
return sortFn;
}
return {
extend: 'Ext.ux.ajax.Simlet',
buildNodes: function(node, path) {
var me = this,
nodeData = {
data: []
},
len = node.length,
children, i, child, name;
me.nodes[path] = nodeData;
for (i = 0; i < len; ++i) {
nodeData.data.push(child = node[i]);
name = child.text || child.title;
child.id = path ? path + '/' + name : name;
children = child.children;
if (!(child.leaf = !children)) {
delete child.children;
me.buildNodes(children, child.id);
}
}
},
deleteRecord: function(pos) {
if (this.data && typeof this.data !== 'function') {
Ext.Array.removeAt(this.data, pos);
}
},
fixTree: function(ctx, tree) {
var me = this,
node = ctx.params.node,
nodes;
if (!(nodes = me.nodes)) {
me.nodes = nodes = {};
me.buildNodes(tree, '');
}
node = nodes[node];
if (node) {
if (me.node) {
me.node.sortedData = me.sortedData;
me.node.currentOrder = me.currentOrder;
}
me.node = node;
me.data = node.data;
me.sortedData = node.sortedData;
me.currentOrder = node.currentOrder;
} else {
me.data = null;
}
},
getData: function(ctx) {
var me = this,
params = ctx.params,
order = (params.filter || '') + (params.group || '') + '-' + (params.sort || '') + '-' + (params.dir || ''),
tree = me.tree,
dynamicData, data, fields, sortFn;
if (tree) {
me.fixTree(ctx, tree);
}
data = me.data;
if (typeof data === 'function') {
dynamicData = true;
data = data.call(this, ctx);
}
// If order is '--' then it means we had no order passed, due to the string concat above
if (!data || order === '--') {
return data || [];
}
if (!dynamicData && order == me.currentOrder) {
return me.sortedData;
}
ctx.filterSpec = params.filter && Ext.decode(params.filter);
ctx.groupSpec = params.group && Ext.decode(params.group);
fields = params.sort;
if (params.dir) {
fields = [
{
direction: params.dir,
property: fields
}
];
} else {
fields = Ext.decode(params.sort);
}
if (ctx.filterSpec) {
var filters = new Ext.util.FilterCollection();
filters.add(this.processFilters(ctx.filterSpec));
data = Ext.Array.filter(data, filters.getFilterFn());
}
sortFn = makeSortFns((ctx.sortSpec = fields));
if (ctx.groupSpec) {
sortFn = makeSortFns([
ctx.groupSpec
], sortFn);
}
// If a straight Ajax request, data may not be an array.
// If an Array, preserve 'physical' order of raw data...
data = Ext.isArray(data) ? data.slice(0) : data;
if (sortFn) {
Ext.Array.sort(data, sortFn);
}
me.sortedData = data;
me.currentOrder = order;
return data;
},
processFilters: Ext.identityFn,
getPage: function(ctx, data) {
var ret = data,
length = data.length,
start = ctx.params.start || 0,
end = ctx.params.limit ? Math.min(length, start + ctx.params.limit) : length;
if (start || end < length) {
ret = ret.slice(start, end);
}
return ret;
},
getGroupSummary: function(groupField, rows, ctx) {
return rows[0];
},
getSummary: function(ctx, data, page) {
var me = this,
groupField = ctx.groupSpec.property,
accum,
todo = {},
summary = [],
fieldValue, lastFieldValue;
Ext.each(page, function(rec) {
fieldValue = rec[groupField];
todo[fieldValue] = true;
});
function flush() {
if (accum) {
summary.push(me.getGroupSummary(groupField, accum, ctx));
accum = null;
}
}
// data is ordered primarily by the groupField, so one pass can pick up all
// the summaries one at a time.
Ext.each(data, function(rec) {
fieldValue = rec[groupField];
if (lastFieldValue !== fieldValue) {
flush();
lastFieldValue = fieldValue;
}
if (!todo[fieldValue]) {
// if we have even 1 summary, we have summarized all that we need
// (again because data and page are ordered by groupField)
return !summary.length;
}
if (accum) {
accum.push(rec);
} else {
accum = [
rec
];
}
return true;
});
flush();
// make sure that last pesky summary goes...
return summary;
}
};
}());
/**
* JSON Simlet.
*/
Ext.define('Ext.ux.ajax.JsonSimlet', {
extend: 'Ext.ux.ajax.DataSimlet',
alias: 'simlet.json',
doGet: function(ctx) {
var me = this,
data = me.getData(ctx),
page = me.getPage(ctx, data),
reader = ctx.xhr.options.proxy && ctx.xhr.options.proxy.getReader(),
root = reader && reader.getRootProperty(),
ret = me.callParent(arguments),
// pick up status/statusText
response = {};
if (root && Ext.isArray(page)) {
response[root] = page;
response[reader.getTotalProperty()] = data.length;
} else {
response = page;
}
if (ctx.groupSpec) {
response.summaryData = me.getSummary(ctx, data, page);
}
ret.responseText = Ext.encode(response);
return ret;
},
doPost: function(ctx) {
return this.doGet(ctx);
}
});
/**
* Pivot Simlet does remote pivot calculations.
* Filtering the pivot results doesn't work.
*/
Ext.define('Ext.ux.ajax.PivotSimlet', {
extend: 'Ext.ux.ajax.JsonSimlet',
alias: 'simlet.pivot',
lastPost: null,
// last Ajax params sent to this simlet
lastResponse: null,
// last JSON response produced by this simlet
keysSeparator: '',
grandTotalKey: '',
doPost: function(ctx) {
var me = this,
ret = me.callParent(arguments);
// pick up status/statusText
me.lastResponse = me.processData(me.getData(ctx), Ext.decode(ctx.xhr.body));
ret.responseText = Ext.encode(me.lastResponse);
return ret;
},
processData: function(data, params) {
var me = this,
len = data.length,
response = {
success: true,
leftAxis: [],
topAxis: [],
results: []
},
leftAxis = new Ext.util.MixedCollection(),
topAxis = new Ext.util.MixedCollection(),
results = new Ext.util.MixedCollection(),
i, j, k, leftKeys, topKeys, item, agg;
me.lastPost = params;
me.keysSeparator = params.keysSeparator;
me.grandTotalKey = params.grandTotalKey;
for (i = 0; i < len; i++) {
leftKeys = me.extractValues(data[i], params.leftAxis, leftAxis);
topKeys = me.extractValues(data[i], params.topAxis, topAxis);
// add record to grand totals
me.addResult(data[i], me.grandTotalKey, me.grandTotalKey, results);
for (j = 0; j < leftKeys.length; j++) {
// add record to col grand totals
me.addResult(data[i], leftKeys[j], me.grandTo