/** * @class Ext.chart.series.Area * @extends Ext.chart.series.Cartesian * * Creates a Stacked Area Chart. The stacked area chart is useful when displaying multiple aggregated layers of information. * As with all other series, the Area Series must be appended in the *series* Chart array configuration. See the Chart * documentation for more information. A typical configuration object for the area series could be: *

   series: [{
       type: 'area',
       highlight: true,
       axis: 'left',
       xField: 'name',
       yField: ['data1', 'data2', 'data3', 'data4', 'data5', 'data6', 'data7'],
       style: {
           opacity: 0.93
       }
   }]
   
* * In this configuration we set `area` as the type for the series, set highlighting options to true for highlighting elements on hover, * take the left axis to measure the data in the area series, set as xField (x values) the name field of each element in the store, * and as yFields (aggregated layers) seven data fields from the same store. Then we override some theming styles by adding some opacity * to the style object. * * @xtype area * */ Ext.define('Ext.chart.series.Area', { /* Begin Definitions */ extend: 'Ext.chart.series.Cartesian', requires: ['Ext.chart.axis.Axis', 'Ext.draw.Color', 'Ext.fx.Anim'], /* End Definitions */ type: 'area',
/** * @cfg {Object} style * Append styling properties to this object for it to override theme properties. */ style: {}, constructor: function(config) { this.callParent(arguments); var me = this, surface = me.chart.surface, i, l; Ext.apply(me, config, { __excludes: [], highlightCfg: { lineWidth: 3, stroke: '#55c', opacity: 0.8, color: '#f00' } }); if (me.highlight) { me.highlightSprite = surface.add({ type: 'path', path: ['M', 0, 0], zIndex: 1000, opacity: 0.3, lineWidth: 5, hidden: true, stroke: '#444' }); } me.group = surface.getGroup(me.seriesId); }, // @private Shrinks dataSets down to a smaller size shrink: function(xValues, yValues, size) { var len = xValues.length, ratio = Math.floor(len / size), i, j, xSum = 0, yCompLen = this.areas.length, ySum = [], xRes = [], yRes = []; //initialize array for (j = 0; j < yCompLen; ++j) { ySum[j] = 0; } for (i = 0; i < len; ++i) { xSum += xValues[i]; for (j = 0; j < yCompLen; ++j) { ySum[j] += yValues[i][j]; } if (i % ratio == 0) { //push averages xRes.push(xSum/ratio); for (j = 0; j < yCompLen; ++j) { ySum[j] /= ratio; } yRes.push(ySum); //reset sum accumulators xSum = 0; for (j = 0, ySum = []; j < yCompLen; ++j) { ySum[j] = 0; } } } return { x: xRes, y: yRes }; }, // @private Get chart and data boundaries getBounds: function() { var me = this, chart = me.chart, store = chart.substore || chart.store, chartBBox = chart.chartBBox, gutterX = chart.maxGutter[0], gutterY = chart.maxGutter[1], areas = [].concat(me.yField), areasLen = areas.length, xValues = [], yValues = [], bbox, axis, minX, maxX, minY, maxY, ends, xScale, yScale, xValue, yValue, areaIndex, acumY, ln, sumValues; //get box dimensions bbox = me.bbox = {}; bbox.x = chartBBox.x + gutterX; bbox.y = chartBBox.y + gutterY; bbox.width = chartBBox.width - (gutterX * 2); bbox.height = chartBBox.height - (gutterY * 2); axis = chart.axes.get(me.axis); if (axis) { if (axis.position == 'top' || axis.position == 'bottom') { minX = axis.from; maxX = axis.to; } else { minY = axis.from; maxY = axis.to; } } else { if (me.xField) { ends = new Ext.chart.axis.Axis({ chart: chart, fields: [me.xField] }).calcEnds(); minX = ends.from; maxX = ends.to; } else if (areas.length) { ends = new Ext.chart.axis.Axis({ chart: chart, fields: [areas] }).calcEnds(); minY = ends.from; maxY = ends.to; } } if (isNaN(minX)) { minX = 0; xScale = bbox.width / (store.getCount() - 1); } else { xScale = bbox.width / (maxX - minX); } if (isNaN(minY)) { minY = 0; yScale = bbox.height / (store.getCount() - 1); } else { yScale = bbox.height / (maxY - minY); } store.each(function(record, i) { if (i == 0) { maxX = 0; maxY = 0; } xValue = record.get(me.xField); yValue = []; if (typeof xValue != 'number') { xValue = i; } xValues.push(xValue); acumY = 0; for (areaIndex = 0; areaIndex < areasLen; areaIndex++) { areaElem = record.get(areas[areaIndex]); acumY += areaElem; if (typeof areaElem == 'number') { yValue.push(areaElem); } else { yValue.push(i); } } maxX = Math.max(maxX, xValue); maxY = Math.max(maxY, acumY); yValues.push(yValue); }, me); xScale = bbox.width / (maxX - minX); yScale = bbox.height / (maxY - minY); ln = xValues.length; if ((ln > bbox.width || ln > bbox.height) && me.areas) { sumValues = me.shrink(xValues, yValues, Math.min(bbox.width, bbox.height)); xValues = sumValues.x; yValues = sumValues.y; } return { bbox: bbox, minX: minX, minY: minY, xValues: xValues, yValues: yValues, xScale: xScale, yScale: yScale, areasLen: areasLen }; }, // @private Build an array of paths for the chart getPaths: function() { var me = this, chart = me.chart, store = chart.substore || chart.store, first = true, bounds = me.getBounds(), bbox = bounds.bbox, items = me.items = [], componentPaths = [], paths = [], i, ln, x, y, xValue, yValue, acumY, areaIndex, prevAreaIndex, areaElem, path; ln = bounds.xValues.length; // Start the path for (i = 0; i < ln; i++) { xValue = bounds.xValues[i]; yValue = bounds.yValues[i]; x = bbox.x + (xValue - bounds.minX) * bounds.xScale; acumY = 0; for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) { // Excluded series if (me.__excludes[areaIndex]) { continue; } if (!componentPaths[areaIndex]) { componentPaths[areaIndex] = []; } areaElem = yValue[areaIndex]; acumY += areaElem; y = bbox.y + bbox.height - (acumY - bounds.minY) * bounds.yScale; if (!paths[areaIndex]) { paths[areaIndex] = ['M', x, y]; componentPaths[areaIndex].push(['L', x, y]); } else { paths[areaIndex].push('L', x, y); componentPaths[areaIndex].push(['L', x, y]); } if (!items[areaIndex]) { items[areaIndex] = { pointsUp: [], pointsDown: [], series: me }; } items[areaIndex].pointsUp.push([x, y]); } } // Close the paths for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) { // Excluded series if (me.__excludes[areaIndex]) { continue; } path = paths[areaIndex]; // Close bottom path to the axis if (areaIndex == 0 || first) { first = false; path.push('L', x, bbox.y + bbox.height, 'L', bbox.x, bbox.y + bbox.height, 'Z'); } // Close other paths to the one before them else { componentPath = componentPaths[prevAreaIndex]; componentPath.reverse(); path.push('L', x, componentPath[0][2]); for (i = 0; i < ln; i++) { path.push(componentPath[i][0], componentPath[i][1], componentPath[i][2]); items[areaIndex].pointsDown[ln -i -1] = [componentPath[i][1], componentPath[i][2]]; } path.push('L', bbox.x, path[2], 'Z'); } prevAreaIndex = areaIndex; } return { paths: paths, areasLen: bounds.areasLen }; },
/** * Draws the series for the current chart. */ drawSeries: function() { var me = this, chart = me.chart, store = chart.substore || chart.store, surface = chart.surface, animate = chart.animate, group = me.group, endLineStyle = Ext.apply(me.seriesStyle, me.style), colorArrayStyle = me.colorArrayStyle, colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0, seriesLabelStyle = me.seriesLabelStyle, areaIndex, areaElem, path, rendererAttributes; me.unHighlightItem(); me.cleanHighlights(); paths = me.getPaths(); if (!me.areas) { me.areas = []; } for (areaIndex = 0; areaIndex < paths.areasLen; areaIndex++) { // Excluded series if (me.__excludes[areaIndex]) { continue; } if (!me.areas[areaIndex]) { me.items[areaIndex].sprite = me.areas[areaIndex] = surface.add(Ext.apply({}, { type: 'path', group: group, path: paths.paths[areaIndex], stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength], fill: colorArrayStyle[areaIndex % colorArrayLength] }, endLineStyle || {})); } areaElem = me.areas[areaIndex]; path = paths.paths[areaIndex]; if (animate) { //Add renderer to line. There is not a unique record associated with this. rendererAttributes = me.renderer(areaElem, false, { path: path, fill: colorArrayStyle[areaIndex % colorArrayLength], stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength] }, areaIndex, store); //fill should not be used here but when drawing the special fill path object me.animation = animation = me.onAnimate(areaElem, { to: rendererAttributes }); } else { rendererAttributes = me.renderer(areaElem, false, { path: path, hidden: false, fill: colorArrayStyle[areaIndex % colorArrayLength], stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength] }, areaIndex, store); me.areas[areaIndex].setAttributes(rendererAttributes, true); } } me.renderLabels(); me.renderCallouts(); }, // @private onAnimate: function(sprite, attr) { sprite.show(); return this.callParent(arguments); }, // @private onCreateLabel: function(storeItem, item, i, display) { var me = this, group = me.labelsGroup, config = me.label, bbox = me.bbox, endLabelStyle = Ext.apply(config, me.seriesLabelStyle); return me.chart.surface.add(Ext.apply({ 'type': 'text', 'text-anchor': 'middle', 'group': group, 'x': item.point[0], 'y': bbox.y + bbox.height / 2 }, endLabelStyle || {})); }, // @private onPlaceLabel: function(label, storeItem, item, i, display, animate, index) { var me = this, chart = me.chart, resizing = chart.resizing, config = me.label, format = config.renderer, field = config.field, bbox = me.bbox, x = item.point[0], y = item.point[1], bb, width, height; label.setAttributes({ text: format(storeItem.get(field[index])), hidden: true }, true); bb = label.getBBox(); width = bb.width / 2; height = bb.height / 2; x = x - width < bbox.x? bbox.x + width : x; x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x; y = y - height < bbox.y? bbox.y + height : y; y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y; if (me.chart.animate && !me.chart.resizing) { label.show(true); me.onAnimate(label, { to: { x: x, y: y } }); } else { label.setAttributes({ x: x, y: y }, true); if (resizing) { me.animation.on('afteranimate', function() { label.show(true); }); } else { label.show(true); } } }, // @private onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) { var me = this, chart = me.chart, surface = chart.surface, resizing = chart.resizing, config = me.callouts, items = me.items, prev = (i == 0) ? false : items[i -1].point, next = (i == items.length -1) ? false : items[i +1].point, cur = item.point, dir, norm, normal, a, aprev, anext, bbox = callout.label.getBBox(), offsetFromViz = 30, offsetToSide = 10, offsetBox = 3, boxx, boxy, boxw, boxh, p, clipRect = me.clipRect, x, y; //get the right two points if (!prev) { prev = cur; } if (!next) { next = cur; } a = (next[1] - prev[1]) / (next[0] - prev[0]); aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]); anext = (next[1] - cur[1]) / (next[0] - cur[0]); norm = Math.sqrt(1 + a * a); dir = [1 / norm, a / norm]; normal = [-dir[1], dir[0]]; //keep the label always on the outer part of the "elbow" if (aprev > 0 && anext < 0 && normal[1] < 0 || aprev < 0 && anext > 0 && normal[1] > 0) { normal[0] *= -1; normal[1] *= -1; } else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0 || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) { normal[0] *= -1; normal[1] *= -1; } //position x = cur[0] + normal[0] * offsetFromViz; y = cur[1] + normal[1] * offsetFromViz; //box position and dimensions boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox)); boxy = y - bbox.height /2 - offsetBox; boxw = bbox.width + 2 * offsetBox; boxh = bbox.height + 2 * offsetBox; //now check if we're out of bounds and invert the normal vector correspondingly //this may add new overlaps between labels (but labels won't be out of bounds). if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) { normal[0] *= -1; } if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) { normal[1] *= -1; } //update positions x = cur[0] + normal[0] * offsetFromViz; y = cur[1] + normal[1] * offsetFromViz; //update box position and dimensions boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox)); boxy = y - bbox.height /2 - offsetBox; boxw = bbox.width + 2 * offsetBox; boxh = bbox.height + 2 * offsetBox; //set the line from the middle of the pie to the box. callout.lines.setAttributes({ path: ["M", cur[0], cur[1], "L", x, y, "Z"] }, true); //set box position callout.box.setAttributes({ x: boxx, y: boxy, width: boxw, height: boxh }, true); //set text position callout.label.setAttributes({ x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)), y: y }, true); for (p in callout) { callout[p].show(true); } },
/** * For a given x/y point relative to the Surface, find a corresponding item from this * series, if any. * * @param x {Number} The left pointer coordinate. * @param y {Number} The top pointer coordinate. * @return {Object} An object with the item found or null instead. */ getItemForPoint: function(x, y) { var me = this, items = me.items, dist = Infinity, tolerance = 20, p, pln, pointsUp, point, abs = Math.abs, bbox = me.bbox, result = null, item, prevItem, nextItem, prevPoint, nextPoint, i, ln, x1, y1, x2, y2, yIntersect; if (x < bbox.x || x > bbox.x + bbox.width || y < bbox.y || y > bbox.y + bbox.height) { return null; } if (items && items.length) { //Find closest point for (i = 0, ln = items.length; i < ln; i++) { item = items[i]; if (item) { pointsUp = item.pointsUp; pointsDown = item.pointsDown; dist = Infinity; for (p = 0, pln = pointsUp.length; p < pln; p++) { point = [pointsUp[p][0], pointsUp[p][1]]; if (dist > abs(x - point[0])) { dist = abs(x - point[0]); } else { point = pointsUp[p -1]; if (y >= point[1] && (!pointsDown.length || y <= (pointsDown[p -1][1]))) { item.storeIndex = p -1; item.storeField = me.yField[i]; item.storeItem = me.chart.store.getAt(p -1); item._points = pointsDown.length? [point, pointsDown[p -1]] : [point]; return item; } else { break; } } } } } } return null; },
/** * Highlight this entire series. * @param {Object} item Info about the item; same format as returned by #getItemForPoint. */ highlightSeries: function() { var area, to, fillColor; if (this._index !== undefined) { area = this.areas[this._index]; if (area.__highlightAnim) { area.__highlightAnim.paused = true; } area.__highlighted = true; area.__prevOpacity = area.__prevOpacity || area.attr.opacity || 1; area.__prevFill = area.__prevFill || area.attr.fill; area.__prevLineWidth = area.__prevLineWidth || area.attr.lineWidth; fillColor = Ext.draw.Color.fromString(area.__prevFill); to = { lineWidth: (area.__prevLineWidth || 0) + 2 }; if (fillColor) { to.fill = fillColor.getLighter(0.2).toString(); } else { to.opacity = Math.max(area.__prevOpacity - 0.3, 0); } if (this.chart.animate) { area.__highlightAnim = new Ext.fx.Anim(Ext.apply({ target: area, to: to }, this.chart.animate)); } else { area.setAttributes(to, true); } } },
/** * UnHighlight this entire series. * @param {Object} item Info about the item; same format as returned by #getItemForPoint. */ unHighlightSeries: function() { var area; if (this._index !== undefined) { area = this.areas[this._index]; if (area.__highlightAnim) { area.__highlightAnim.paused = true; } if (area.__highlighted) { area.__highlighted = false; area.__highlightAnim = new Ext.fx.Anim({ target: area, to: { fill: area.__prevFill, opacity: area.__prevOpacity, lineWidth: area.__prevLineWidth } }); } } },
/** * Highlight the specified item. If no item is provided the whole series will be highlighted. * @param item {Object} Info about the item; same format as returned by #getItemForPoint */ highlightItem: function(item) { var me = this, points, path; if (!item) { this.highlightSeries(); return; } points = item._points; path = points.length == 2? ['M', points[0][0], points[0][1], 'L', points[1][0], points[1][1]] : ['M', points[0][0], points[0][1], 'L', points[0][0], me.bbox.y + me.bbox.height]; me.highlightSprite.setAttributes({ path: path, hidden: false }, true); },
/** * un-highlights the specified item. If no item is provided it will un-highlight the entire series. * @param item {Object} Info about the item; same format as returned by #getItemForPoint */ unHighlightItem: function(item) { if (!item) { this.unHighlightSeries(); } if (this.highlightSprite) { this.highlightSprite.hide(true); } }, // @private hideAll: function() { if (!isNaN(this._index)) { this.__excludes[this._index] = true; this.areas[this._index].hide(true); this.drawSeries(); } }, // @private showAll: function() { if (!isNaN(this._index)) { this.__excludes[this._index] = false; this.areas[this._index].show(true); this.drawSeries(); } },
/** * 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 me = this; return me.colorArrayStyle[index % me.colorArrayStyle.length]; } });