/** * Displays a gauge chart. * * @example * Ext.create({ * xtype: 'polar', * renderTo: document.body, * width: 600, * height: 400, * store: { * fields: ['mph', 'fuel', 'temp', 'rpm'], * data: [{ * mph: 65, * fuel: 50, * temp: 150, * rpm: 6000 * }] * }, * series: { * type: 'gauge', * colors: ['#1F6D91', '#90BCC9'], * angleField: 'mph', * needle: true, * donut: 30 * } * }); */Ext.define('Ext.chart.series.Gauge', { alias: 'series.gauge', extend: 'Ext.chart.series.Polar', type: 'gauge', seriesType: 'pieslice', requires: [ 'Ext.draw.sprite.Sector' ], config: { /** * @cfg {String} angleField * The store record field name to be used for the gauge value. * The values bound to this field name must be positive real numbers. */ /** * @cfg {Boolean} needle * If true, display the gauge as a needle, otherwise as a sector. */ needle: false, /** * @cfg {Number} needleLength * Percentage of the length of needle compared to the radius of the entire disk. */ needleLength: 90, /** * @cfg {Number} needleWidth * Width of the needle in pixels. */ needleWidth: 4, /** * @cfg {Number} donut * Percentage of the radius of the donut hole compared to the entire disk. */ donut: 30, /** * @cfg {Boolean} showInLegend * Whether to add the gauge chart elements as legend items. */ showInLegend: false, /** * @cfg {Number} value * Directly sets the displayed value of the gauge. * It is ignored if {@link #angleField} is provided. */ value: null, /** * @cfg {Array} colors (required) * An array of color values which is used for the needle and the `sectors`. */ colors: null, /** * @cfg {Array} sectors * Allows to paint sectors of different colors in the background of the gauge, * with optional labels. * * It can be an array of numbers (each between `minimum` and `maximum`) that * define the highest value of each sector. For N sectors, only (N-1) values are * needed because it is assumed that the first sector starts at `minimum` and the * last sector ends at `maximum`. Example: a water temperature gauge that is blue * below 20C, red above 80C, gray in-between, and with an orange needle... * * minimum: 0, * maximum: 100, * sectors: [20, 80], * colors: ['orange', 'blue', 'lightgray', 'red'] * * It can be also an array of objects, each with the following properties: * * @cfg {Number} sectors.start The starting value of the sector. If omitted, it * uses the previous sector's `end` value or the chart's `minimum`. * @cfg {Number} sectors.end The ending value of the sector. If omitted, it uses * the `maximum` defined for the chart. * @cfg {String} sectors.label The label for this sector. Labels are styled using * the series' {@link Ext.chart.series.Series#label label} config. * @cfg {String} sectors.color The color of the sector. If omitted, it uses one * of the `colors` defined for the series or for the chart. * @cfg {Object} sectors.style An additional style object for the sector (for * instance to set the opacity or to draw a line of a different color around the * sector). * * minimum: 0, * maximum: 100, * sectors: [{ * end: 20, * label: 'Cold', * color: 'aqua' * }, * { * end: 80, * label: 'Temp.', * color: 'lightgray', * style: { strokeStyle:'black', strokeOpacity:1, lineWidth:1 } * }, * { * label: 'Hot', * color: 'tomato' * }] */ sectors: null, /** * @cfg {Number} minimum * The minimum value of the gauge. */ minimum: 0, /** * @cfg {Number} maximum * The maximum value of the gauge. */ maximum: 100, rotation: 0, /** * @cfg {Number} totalAngle * The size of the sector that the series will occupy. */ totalAngle: Math.PI / 2, rect: [0, 0, 1, 1], center: [0.5, 0.75], radius: 0.5, /** * @cfg {Boolean} wholeDisk Indicates whether to show the whole disk or only the marked part. */ wholeDisk: false }, coordinateX: function () { return this.coordinate('X', 0, 2); }, coordinateY: function () { return this.coordinate('Y', 1, 2); }, updateNeedle: function(needle) { var me = this, sprites = me.getSprites(), angle = me.valueToAngle(me.getValue()); if (sprites && sprites.length) { sprites[0].setAttributes({ startAngle: (needle ? angle : 0), endAngle: angle, strokeOpacity: (needle ? 1 : 0), lineWidth: (needle ? me.getNeedleWidth() : 0) }); me.doUpdateStyles(); } }, themeColorCount: function() { var me = this, store = me.getStore(), count = store && store.getCount() || 0; return count + (me.getNeedle() ? 0 : 1); }, updateColors: function (colors, oldColors) { var me = this, sectors = me.getSectors(), sectorCount = sectors && sectors.length, sprites = me.getSprites(), newColors = Ext.Array.clone(colors), colorCount = colors && colors.length, i; if (!colorCount || !colors[0]) { return; } // Make sure the 'sectors' colors are not overridden. for (i = 0; i < sectorCount; i++) { newColors[i+1] = sectors[i].color || newColors[i+1] || colors[i%colorCount]; } if (sprites.length) { sprites[0].setAttributes({ strokeStyle: newColors[0] }); } this.setSubStyle({ fillStyle: newColors, strokeStyle: newColors }); this.doUpdateStyles(); }, updateRect: function (rect) { var wholeDisk = this.getWholeDisk(), halfTotalAngle = wholeDisk ? Math.PI : this.getTotalAngle() / 2, donut = this.getDonut() / 100, width, height, radius; if (halfTotalAngle <= Math.PI / 2) { width = 2 * Math.sin(halfTotalAngle); height = 1 - donut * Math.cos(halfTotalAngle); } else { width = 2; height = 1 - Math.cos(halfTotalAngle); } radius = Math.min(rect[2] / width, rect[3] / height); this.setRadius(radius); this.setCenter([rect[2] / 2, radius + (rect[3] - height * radius) / 2]); }, updateCenter: function (center) { this.setStyle({ centerX: center[0], centerY: center[1], rotationCenterX: center[0], rotationCenterY: center[1] }); this.doUpdateStyles(); }, updateRotation: function (rotation) { this.setStyle({ rotationRads: rotation - (this.getTotalAngle() + Math.PI) / 2 }); this.doUpdateStyles(); }, doUpdateShape: function (radius, donut) { var me = this, sectors = me.getSectors(), sectorCount = (sectors && sectors.length) || 0, needleLength = me.getNeedleLength() / 100, endRhoArray; // Initialize an array that contains the endRho for each sprite. // The first sprite is for the needle, the others for the gauge background sectors. // Note: SubStyle arrays are handled in series.getStyleByIndex(). endRhoArray = [radius * needleLength, radius]; while (sectorCount --) { endRhoArray.push(radius); } me.setSubStyle({ endRho: endRhoArray, startRho: radius / 100 * donut }); me.doUpdateStyles(); }, updateRadius: function (radius) { var donut = this.getDonut(); this.doUpdateShape(radius, donut); }, updateDonut: function (donut) { var radius = this.getRadius(); this.doUpdateShape(radius, donut); }, valueToAngle: function(value) { value = this.applyValue(value); return this.getTotalAngle() * (value - this.getMinimum()) / (this.getMaximum() - this.getMinimum()); }, applyValue: function (value) { return Math.min(this.getMaximum(), Math.max(value, this.getMinimum())); }, updateValue: function (value) { var me = this, needle = me.getNeedle(), angle = me.valueToAngle(value), sprites = me.getSprites(); sprites[0].getRendererData().value = value; sprites[0].setAttributes({ startAngle: (needle ? angle : 0), endAngle: angle }); me.doUpdateStyles(); }, processData: function () { var me = this, store = me.getStore(), record = store && store.first(), animation, duration, axis, min, max, xField, value; if (record) { xField = me.getXField(); if (xField) { value = record.get(xField); } } if (axis = me.getXAxis()) { min = axis.getMinimum(); max = axis.getMaximum(); // Animating the axis here can lead to weird looking results. animation = axis.getSprites()[0].getAnimation(); duration = animation.getDuration(); animation.setDuration(0); if (Ext.isNumber(min)) { me.setMinimum(min); } else { axis.setMinimum(me.getMinimum()); } if (Ext.isNumber(max)) { me.setMaximum(max); } else { axis.setMaximum(me.getMaximum()); } animation.setDuration(duration); } if (!Ext.isNumber(value)) { value = me.getMinimum(); } me.setValue(value); }, getDefaultSpriteConfig: function () { return { type: this.seriesType, renderer: this.getRenderer(), animation: { customDurations: { translationX: 0, translationY: 0, rotationCenterX: 0, rotationCenterY: 0, centerX: 0, centerY: 0, startRho: 0, endRho: 0, baseRotation: 0 } } }; }, normalizeSectors: function(sectors) { // Make sure all the sectors in the array have a legit start and end. // Note: the array is modified in-place. var me = this, sectorCount = (sectors && sectors.length) || 0, i, value, start, end; if (sectorCount) { for (i = 0; i < sectorCount; i++) { value = sectors[i]; if (typeof value === 'number') { sectors[i] = { start: (i > 0 ? sectors[i-1].end : me.getMinimum()), end: Math.min(value, me.getMaximum()) }; if (i == (sectorCount - 1) && sectors[i].end < me.getMaximum()) { sectors[i+1] = { start: sectors[i].end, end: me.getMaximum() }; } } else { if (typeof value.start === 'number') { start = Math.max(value.start, me.getMinimum()); } else { start = (i > 0 ? sectors[i-1].end : me.getMinimum()); } if (typeof value.end === 'number') { end = Math.min(value.end, me.getMaximum()); } else { end = me.getMaximum(); } sectors[i].start = start; sectors[i].end = end; } } } else { sectors = [{ start: me.getMinimum(), end: me.getMaximum() }]; } return sectors; }, getSprites: function () { var me = this, store = me.getStore(), value = me.getValue(), label = me.getLabel(), i, ln; // The store must be initialized, or the value must be set if (!store && !Ext.isNumber(value)) { return Ext.emptyArray; } // Return cached sprites var chart = me.getChart(), animation = me.getAnimation() || chart && chart.getAnimation(), sprites = me.sprites, spriteIndex = 0, sprite, sectors, attr, rendererData, lineWidths = []; // Hack to avoid having the lineWidths overwritten by the one specified in the theme. // In fact, all the style properties from the needle and sectors should go to the series subStyle. if (sprites && sprites.length) { sprites[0].setAnimation(animation); return sprites; } rendererData = { store: store, field: me.getXField(), // for backward compatibility only (deprecated in 5.5) angleField: me.getXField(), value: value, series: me }; // Create needle sprite me.needleSprite = sprite = me.createSprite(); sprite.setAttributes({ zIndex: 10 }, true); sprite.setRendererData(rendererData); sprite.setRendererIndex(spriteIndex++); lineWidths.push(me.getNeedleWidth()); if (label) { label.getTemplate().setField(true); // Enable labels } // Create background sprite(s) sectors = me.normalizeSectors(me.getSectors()); for (i = 0, ln = sectors.length; i < ln; i++) { attr = { startAngle: me.valueToAngle(sectors[i].start), endAngle: me.valueToAngle(sectors[i].end), label: sectors[i].label, fillStyle: sectors[i].color, strokeOpacity: 0, doCallout: false, // Show labels inside sectors. labelOverflowPadding: -1 // Allow labels to overlap. }; Ext.apply(attr, sectors[i].style); sprite = me.createSprite(); sprite.setRendererData(rendererData); sprite.setRendererIndex(spriteIndex++); sprite.setAttributes(attr, true); lineWidths.push(attr.lineWidth); } me.setSubStyle({ lineWidth: lineWidths }); me.doUpdateStyles(); return sprites; }, doUpdateStyles: function () { var me = this; me.callParent(); if (me.sprites.length) { me.needleSprite.setAttributes({ startRho: me.getNeedle() ? 0 : (me.getRadius() / 100 * me.getDonut()) }); } } });