/*
This file is part of Ext JS 4.2
Copyright (c) 2011-2013 Sencha Inc
Contact: http://www.sencha.com/contact
GNU General Public License Usage
This file may be used under the terms of the GNU General Public License version 3.0 as
published by the Free Software Foundation and appearing in the file LICENSE included in the
packaging of this file.
Please review the following information to ensure the GNU General Public License version 3.0
requirements will be met: http://www.gnu.org/copyleft/gpl.html.
If you are unsure which license is appropriate for your use, please contact the sales department
at http://www.sencha.com/contact.
Build date: 2013-05-16 14:36:50 (f9be68accb407158ba2b1be2c226a6ce1f649314)
*/
/**
* @class Ext.chart.series.Gauge
*
* Creates a Gauge Chart. Gauge Charts are used to show progress in a certain variable. There are two ways of using the Gauge chart.
* One is setting a store element into the Gauge and selecting the field to be used from that store. Another one is instantiating the
* visualization and using the `setValue` method to adjust the value you want.
*
* An example of Gauge visualization:
*
* @example
* var store = Ext.create('Ext.data.JsonStore', {
* fields: ['value'],
* data: [
* { 'value':80 }
* ]
* });
*
* Ext.create('Ext.chart.Chart', {
* renderTo: Ext.getBody(),
* store: store,
* width: 400,
* height: 250,
* animate: true,
* insetPadding: 30,
* axes: [{
* type: 'gauge',
* position: 'gauge',
* minimum: 0,
* maximum: 100,
* steps: 10,
* margin: 10
* }],
* series: [{
* type: 'gauge',
* field: 'value',
* donut: 30,
* colorSet: ['#F49D10', '#ddd']
* }]
* });
*
* Ext.widget("button", {
* renderTo: Ext.getBody(),
* text: "Refresh",
* handler: function() {
* store.getAt(0).set('value', Math.round(Math.random()*100));
* }
* });
*
* In this example we create a special Gauge axis to be used with the gauge visualization (describing half-circle markers), and also we're
* setting a maximum, minimum and steps configuration options into the axis. The Gauge series configuration contains the store field to be bound to
* the visual display and the color set to be used with the visualization.
*/
Ext.define('Ext.chart.series.Gauge', {
/* Begin Definitions */
extend: 'Ext.chart.series.Series',
/* End Definitions */
type: "gauge",
alias: 'series.gauge',
rad: Math.PI / 180,
/**
* @cfg {Number} highlightDuration
* The duration for the pie slice highlight effect.
*/
highlightDuration: 150,
/**
* @cfg {String} angleField (required)
* The store record field name to be used for the pie angles.
* The values bound to this field name must be positive real numbers.
*/
angleField: false,
/**
* @cfg {Boolean} needle
* Use the Gauge Series as an area series or add a needle to it. Default's false.
*/
needle: false,
/**
* @cfg {Boolean/Number} donut
* Use the entire disk or just a fraction of it for the gauge. Default's false.
*/
donut: false,
/**
* @cfg {Boolean} showInLegend
* Whether to add the pie chart elements as legend items. Default's false.
*/
showInLegend: false,
/**
* @cfg {Object} style
* An object containing styles for overriding series styles from Theming.
*/
style: {},
constructor: function(config) {
this.callParent(arguments);
var me = this,
chart = me.chart,
surface = chart.surface,
store = chart.store,
shadow = chart.shadow, i, l, cfg;
Ext.apply(me, config, {
shadowAttributes: [{
"stroke-width": 6,
"stroke-opacity": 1,
stroke: 'rgb(200, 200, 200)',
translate: {
x: 1.2,
y: 2
}
},
{
"stroke-width": 4,
"stroke-opacity": 1,
stroke: 'rgb(150, 150, 150)',
translate: {
x: 0.9,
y: 1.5
}
},
{
"stroke-width": 2,
"stroke-opacity": 1,
stroke: 'rgb(100, 100, 100)',
translate: {
x: 0.6,
y: 1
}
}]
});
me.group = surface.getGroup(me.seriesId);
if (shadow) {
for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
}
}
surface.customAttributes.segment = function(opt) {
return me.getSegment(opt);
};
},
// @private updates some onbefore render parameters.
initialize: function() {
var me = this,
store = me.chart.getChartStore(),
data = store.data.items,
label = me.label,
ln = data.length;
me.yField = [];
if (label && label.field && ln > 0) {
me.yField.push(data[0].get(label.field));
}
},
// @private returns an object with properties for a Slice
getSegment: function(opt) {
var me = this,
rad = me.rad,
cos = Math.cos,
sin = Math.sin,
abs = Math.abs,
x = me.centerX,
y = me.centerY,
x1 = 0, x2 = 0, x3 = 0, x4 = 0,
y1 = 0, y2 = 0, y3 = 0, y4 = 0,
delta = 1e-2,
r = opt.endRho - opt.startRho,
startAngle = opt.startAngle,
endAngle = opt.endAngle,
midAngle = (startAngle + endAngle) / 2 * rad,
margin = opt.margin || 0,
flag = abs(endAngle - startAngle) > 180,
a1 = Math.min(startAngle, endAngle) * rad,
a2 = Math.max(startAngle, endAngle) * rad,
singleSlice = false;
x += margin * cos(midAngle);
y += margin * sin(midAngle);
x1 = x + opt.startRho * cos(a1);
y1 = y + opt.startRho * sin(a1);
x2 = x + opt.endRho * cos(a1);
y2 = y + opt.endRho * sin(a1);
x3 = x + opt.startRho * cos(a2);
y3 = y + opt.startRho * sin(a2);
x4 = x + opt.endRho * cos(a2);
y4 = y + opt.endRho * sin(a2);
if (abs(x1 - x3) <= delta && abs(y1 - y3) <= delta) {
singleSlice = true;
}
//Solves mysterious clipping bug with IE
if (singleSlice) {
return {
path: [
["M", x1, y1],
["L", x2, y2],
["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4],
["Z"]]
};
} else {
return {
path: [
["M", x1, y1],
["L", x2, y2],
["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4],
["L", x3, y3],
["A", opt.startRho, opt.startRho, 0, +flag, 0, x1, y1],
["Z"]]
};
}
},
// @private utility function to calculate the middle point of a pie slice.
calcMiddle: function(item) {
var me = this,
rad = me.rad,
slice = item.slice,
x = me.centerX,
y = me.centerY,
startAngle = slice.startAngle,
endAngle = slice.endAngle,
radius = Math.max(('rho' in slice) ? slice.rho: me.radius, me.label.minMargin),
donut = +me.donut,
a1 = Math.min(startAngle, endAngle) * rad,
a2 = Math.max(startAngle, endAngle) * rad,
midAngle = -(a1 + (a2 - a1) / 2),
xm = x + (item.endRho + item.startRho) / 2 * Math.cos(midAngle),
ym = y - (item.endRho + item.startRho) / 2 * Math.sin(midAngle);
item.middle = {
x: xm,
y: ym
};
},
/**
* Draws the series for the current chart.
*/
drawSeries: function() {
var me = this,
chart = me.chart,
store = chart.getChartStore(),
group = me.group,
animate = me.chart.animate,
axis = me.chart.axes.get(0),
minimum = axis && axis.minimum || me.minimum || 0,
maximum = axis && axis.maximum || me.maximum || 0,
field = me.angleField || me.field || me.xField,
surface = chart.surface,
chartBBox = chart.chartBBox,
rad = me.rad,
donut = +me.donut,
values = {},
items = [],
seriesStyle = me.seriesStyle,
seriesLabelStyle = me.seriesLabelStyle,
colorArrayStyle = me.colorArrayStyle,
colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
cos = Math.cos,
sin = Math.sin,
rendererAttributes, centerX, centerY, slice, slices, sprite, value,
item, ln, record, i, j, startAngle, endAngle, middleAngle, sliceLength, path,
p, spriteOptions, bbox, splitAngle, sliceA, sliceB;
Ext.apply(seriesStyle, me.style || {});
me.setBBox();
bbox = me.bbox;
//override theme colors
if (me.colorSet) {
colorArrayStyle = me.colorSet;
colorArrayLength = colorArrayStyle.length;
}
//if not store or store is empty then there's nothing to draw
if (!store || !store.getCount() || me.seriesIsHidden) {
me.hide();
me.items = [];
return;
}
centerX = me.centerX = chartBBox.x + (chartBBox.width / 2);
centerY = me.centerY = chartBBox.y + chartBBox.height;
me.radius = Math.min(centerX - chartBBox.x, centerY - chartBBox.y);
me.slices = slices = [];
me.items = items = [];
if (!me.value) {
record = store.getAt(0);
me.value = record.get(field);
}
value = me.value;
if (me.needle) {
sliceA = {
series: me,
value: value,
startAngle: -180,
endAngle: 0,
rho: me.radius
};
splitAngle = -180 * (1 - (value - minimum) / (maximum - minimum));
slices.push(sliceA);
} else {
splitAngle = -180 * (1 - (value - minimum) / (maximum - minimum));
sliceA = {
series: me,
value: value,
startAngle: -180,
endAngle: splitAngle,
rho: me.radius
};
sliceB = {
series: me,
value: me.maximum - value,
startAngle: splitAngle,
endAngle: 0,
rho: me.radius
};
slices.push(sliceA, sliceB);
}
//do pie slices after.
for (i = 0, ln = slices.length; i < ln; i++) {
slice = slices[i];
sprite = group.getAt(i);
//set pie slice properties
rendererAttributes = Ext.apply({
segment: {
startAngle: slice.startAngle,
endAngle: slice.endAngle,
margin: 0,
rho: slice.rho,
startRho: slice.rho * +donut / 100,
endRho: slice.rho
}
}, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[i % colorArrayLength] } || {}));
item = Ext.apply({},
rendererAttributes.segment, {
slice: slice,
series: me,
storeItem: record,
index: i
});
items[i] = item;
// Create a new sprite if needed (no height)
if (!sprite) {
spriteOptions = Ext.apply({
type: "path",
group: group
}, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[i % colorArrayLength] } || {}));
sprite = surface.add(Ext.apply(spriteOptions, rendererAttributes));
}
slice.sprite = slice.sprite || [];
item.sprite = sprite;
slice.sprite.push(sprite);
if (animate) {
rendererAttributes = me.renderer(sprite, record, rendererAttributes, i, store);
sprite._to = rendererAttributes;
me.onAnimate(sprite, {
to: rendererAttributes
});
} else {
rendererAttributes = me.renderer(sprite, record, Ext.apply(rendererAttributes, {
hidden: false
}), i, store);
sprite.setAttributes(rendererAttributes, true);
}
}
if (me.needle) {
splitAngle = splitAngle * Math.PI / 180;
if (!me.needleSprite) {
me.needleSprite = me.chart.surface.add({
type: 'path',
path: ['M', centerX + (me.radius * +donut / 100) * cos(splitAngle),
centerY + -Math.abs((me.radius * +donut / 100) * sin(splitAngle)),
'L', centerX + me.radius * cos(splitAngle),
centerY + -Math.abs(me.radius * sin(splitAngle))],
'stroke-width': 4,
'stroke': '#222'
});
} else {
if (animate) {
me.onAnimate(me.needleSprite, {
to: {
path: ['M', centerX + (me.radius * +donut / 100) * cos(splitAngle),
centerY + -Math.abs((me.radius * +donut / 100) * sin(splitAngle)),
'L', centerX + me.radius * cos(splitAngle),
centerY + -Math.abs(me.radius * sin(splitAngle))]
}
});
} else {
me.needleSprite.setAttributes({
type: 'path',
path: ['M', centerX + (me.radius * +donut / 100) * cos(splitAngle),
centerY + -Math.abs((me.radius * +donut / 100) * sin(splitAngle)),
'L', centerX + me.radius * cos(splitAngle),
centerY + -Math.abs(me.radius * sin(splitAngle))]
});
}
}
me.needleSprite.setAttributes({
hidden: false
}, true);
}
delete me.value;
},
/**
* Sets the Gauge chart to the current specified value.
*/
setValue: function (value) {
this.value = value;
this.drawSeries();
},
// @private callback for when creating a label sprite.
onCreateLabel: function(storeItem, item, i, display) {},
// @private callback for when placing a label sprite.
onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {},
// @private callback for when placing a callout.
onPlaceCallout: function() {},
// @private handles sprite animation for the series.
onAnimate: function(sprite, attr) {
sprite.show();
return this.callParent(arguments);
},
isItemInPoint: function(x, y, item, i) {
var me = this,
cx = me.centerX,
cy = me.centerY,
abs = Math.abs,
dx = abs(x - cx),
dy = abs(y - cy),
startAngle = item.startAngle,
endAngle = item.endAngle,
rho = Math.sqrt(dx * dx + dy * dy),
angle = Math.atan2(y - cy, x - cx) / me.rad;
//Only trigger events for the filled portion of the Gauge.
return (i === 0) && (angle >= startAngle && angle < endAngle &&
rho >= item.startRho && rho <= item.endRho);
},
/**
* Returns the color of the series (to be displayed as color for the series legend item).
* @param item {Object} Info about the item; same format as returned by #getItemForPoint
*/
getLegendColor: function(index) {
var colors = this.colorSet || this.colorArrayStyle;
return colors[index % colors.length];
}
});
|