3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 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.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.chart.series.Area
17 * @extends Ext.chart.series.Cartesian
20 Creates a Stacked Area Chart. The stacked area chart is useful when displaying multiple aggregated layers of information.
21 As with all other series, the Area Series must be appended in the *series* Chart array configuration. See the Chart
22 documentation for more information. A typical configuration object for the area series could be:
24 {@img Ext.chart.series.Area/Ext.chart.series.Area.png Ext.chart.series.Area chart series}
26 var store = Ext.create('Ext.data.JsonStore', {
27 fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
29 {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
30 {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
31 {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
32 {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
33 {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
37 Ext.create('Ext.chart.Chart', {
38 renderTo: Ext.getBody(),
46 fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
47 title: 'Sample Values',
57 adjustMinimumByMajorUnit: 0
62 title: 'Sample Metrics',
75 yField: ['data1', 'data2', 'data3', 'data4', 'data5'],
85 In this configuration we set `area` as the type for the series, set highlighting options to true for highlighting elements on hover,
86 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,
87 and as yFields (aggregated layers) seven data fields from the same store. Then we override some theming styles by adding some opacity
94 Ext.define('Ext.chart.series.Area', {
96 /* Begin Definitions */
98 extend: 'Ext.chart.series.Cartesian',
100 alias: 'series.area',
102 requires: ['Ext.chart.axis.Axis', 'Ext.draw.Color', 'Ext.fx.Anim'],
104 /* End Definitions */
108 // @private Area charts are alyways stacked
112 * @cfg {Object} style
113 * Append styling properties to this object for it to override theme properties.
117 constructor: function(config) {
118 this.callParent(arguments);
120 surface = me.chart.surface,
122 Ext.apply(me, config, {
132 me.highlightSprite = surface.add({
142 me.group = surface.getGroup(me.seriesId);
145 // @private Shrinks dataSets down to a smaller size
146 shrink: function(xValues, yValues, size) {
147 var len = xValues.length,
148 ratio = Math.floor(len / size),
151 yCompLen = this.areas.length,
156 for (j = 0; j < yCompLen; ++j) {
159 for (i = 0; i < len; ++i) {
161 for (j = 0; j < yCompLen; ++j) {
162 ySum[j] += yValues[i][j];
164 if (i % ratio == 0) {
166 xRes.push(xSum/ratio);
167 for (j = 0; j < yCompLen; ++j) {
171 //reset sum accumulators
173 for (j = 0, ySum = []; j < yCompLen; ++j) {
184 // @private Get chart and data boundaries
185 getBounds: function() {
188 store = chart.substore || chart.store,
189 areas = [].concat(me.yField),
190 areasLen = areas.length,
201 bbox, xScale, yScale, xValue, yValue, areaIndex, acumY, ln, sumValues, clipBox, areaElem;
206 // Run through the axis
208 axis = chart.axes.get(me.axis);
210 out = axis.calcEnds();
211 minY = out.from || axis.prevMin;
212 maxY = mmax(out.to || axis.prevMax, 0);
216 if (me.yField && !Ext.isNumber(minY)) {
217 axis = Ext.create('Ext.chart.axis.Axis', {
219 fields: [].concat(me.yField)
221 out = axis.calcEnds();
222 minY = out.from || axis.prevMin;
223 maxY = mmax(out.to || axis.prevMax, 0);
226 if (!Ext.isNumber(minY)) {
229 if (!Ext.isNumber(maxY)) {
233 store.each(function(record, i) {
234 xValue = record.get(me.xField);
236 if (typeof xValue != 'number') {
239 xValues.push(xValue);
241 for (areaIndex = 0; areaIndex < areasLen; areaIndex++) {
242 areaElem = record.get(areas[areaIndex]);
243 if (typeof areaElem == 'number') {
244 minY = mmin(minY, areaElem);
245 yValue.push(areaElem);
249 minX = mmin(minX, xValue);
250 maxX = mmax(maxX, xValue);
251 maxY = mmax(maxY, acumY);
252 yValues.push(yValue);
255 xScale = bbox.width / (maxX - minX);
256 yScale = bbox.height / (maxY - minY);
259 if ((ln > bbox.width) && me.areas) {
260 sumValues = me.shrink(xValues, yValues, bbox.width);
261 xValues = sumValues.x;
262 yValues = sumValues.y;
277 // @private Build an array of paths for the chart
278 getPaths: function() {
281 store = chart.substore || chart.store,
283 bounds = me.getBounds(),
285 items = me.items = [],
289 i, ln, x, y, xValue, yValue, acumY, areaIndex, prevAreaIndex, areaElem, path;
291 ln = bounds.xValues.length;
293 for (i = 0; i < ln; i++) {
294 xValue = bounds.xValues[i];
295 yValue = bounds.yValues[i];
296 x = bbox.x + (xValue - bounds.minX) * bounds.xScale;
298 for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
300 if (me.__excludes[areaIndex]) {
303 if (!componentPaths[areaIndex]) {
304 componentPaths[areaIndex] = [];
306 areaElem = yValue[areaIndex];
308 y = bbox.y + bbox.height - (acumY - bounds.minY) * bounds.yScale;
309 if (!paths[areaIndex]) {
310 paths[areaIndex] = ['M', x, y];
311 componentPaths[areaIndex].push(['L', x, y]);
313 paths[areaIndex].push('L', x, y);
314 componentPaths[areaIndex].push(['L', x, y]);
316 if (!items[areaIndex]) {
323 items[areaIndex].pointsUp.push([x, y]);
328 for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
330 if (me.__excludes[areaIndex]) {
333 path = paths[areaIndex];
334 // Close bottom path to the axis
335 if (areaIndex == 0 || first) {
337 path.push('L', x, bbox.y + bbox.height,
338 'L', bbox.x, bbox.y + bbox.height,
341 // Close other paths to the one before them
343 componentPath = componentPaths[prevAreaIndex];
344 componentPath.reverse();
345 path.push('L', x, componentPath[0][2]);
346 for (i = 0; i < ln; i++) {
347 path.push(componentPath[i][0],
349 componentPath[i][2]);
350 items[areaIndex].pointsDown[ln -i -1] = [componentPath[i][1], componentPath[i][2]];
352 path.push('L', bbox.x, path[2], 'Z');
354 prevAreaIndex = areaIndex;
358 areasLen: bounds.areasLen
363 * Draws the series for the current chart.
365 drawSeries: function() {
368 store = chart.substore || chart.store,
369 surface = chart.surface,
370 animate = chart.animate,
372 endLineStyle = Ext.apply(me.seriesStyle, me.style),
373 colorArrayStyle = me.colorArrayStyle,
374 colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
375 areaIndex, areaElem, paths, path, rendererAttributes;
377 me.unHighlightItem();
378 me.cleanHighlights();
380 if (!store || !store.getCount()) {
384 paths = me.getPaths();
390 for (areaIndex = 0; areaIndex < paths.areasLen; areaIndex++) {
392 if (me.__excludes[areaIndex]) {
395 if (!me.areas[areaIndex]) {
396 me.items[areaIndex].sprite = me.areas[areaIndex] = surface.add(Ext.apply({}, {
399 // 'clip-rect': me.clipBox,
400 path: paths.paths[areaIndex],
401 stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength],
402 fill: colorArrayStyle[areaIndex % colorArrayLength]
403 }, endLineStyle || {}));
405 areaElem = me.areas[areaIndex];
406 path = paths.paths[areaIndex];
408 //Add renderer to line. There is not a unique record associated with this.
409 rendererAttributes = me.renderer(areaElem, false, {
411 // 'clip-rect': me.clipBox,
412 fill: colorArrayStyle[areaIndex % colorArrayLength],
413 stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength]
414 }, areaIndex, store);
415 //fill should not be used here but when drawing the special fill path object
416 me.animation = me.onAnimate(areaElem, {
417 to: rendererAttributes
420 rendererAttributes = me.renderer(areaElem, false, {
422 // 'clip-rect': me.clipBox,
424 fill: colorArrayStyle[areaIndex % colorArrayLength],
425 stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength]
426 }, areaIndex, store);
427 me.areas[areaIndex].setAttributes(rendererAttributes, true);
435 onAnimate: function(sprite, attr) {
437 return this.callParent(arguments);
441 onCreateLabel: function(storeItem, item, i, display) {
443 group = me.labelsGroup,
446 endLabelStyle = Ext.apply(config, me.seriesLabelStyle);
448 return me.chart.surface.add(Ext.apply({
450 'text-anchor': 'middle',
453 'y': bbox.y + bbox.height / 2
454 }, endLabelStyle || {}));
458 onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {
461 resizing = chart.resizing,
463 format = config.renderer,
464 field = config.field,
470 label.setAttributes({
471 text: format(storeItem.get(field[index])),
475 bb = label.getBBox();
476 width = bb.width / 2;
477 height = bb.height / 2;
479 x = x - width < bbox.x? bbox.x + width : x;
480 x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
481 y = y - height < bbox.y? bbox.y + height : y;
482 y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
484 if (me.chart.animate && !me.chart.resizing) {
486 me.onAnimate(label, {
493 label.setAttributes({
498 me.animation.on('afteranimate', function() {
508 onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) {
511 surface = chart.surface,
512 resizing = chart.resizing,
513 config = me.callouts,
515 prev = (i == 0) ? false : items[i -1].point,
516 next = (i == items.length -1) ? false : items[i +1].point,
518 dir, norm, normal, a, aprev, anext,
519 bbox = callout.label.getBBox(),
523 boxx, boxy, boxw, boxh,
524 p, clipRect = me.clipRect,
527 //get the right two points
534 a = (next[1] - prev[1]) / (next[0] - prev[0]);
535 aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]);
536 anext = (next[1] - cur[1]) / (next[0] - cur[0]);
538 norm = Math.sqrt(1 + a * a);
539 dir = [1 / norm, a / norm];
540 normal = [-dir[1], dir[0]];
542 //keep the label always on the outer part of the "elbow"
543 if (aprev > 0 && anext < 0 && normal[1] < 0 || aprev < 0 && anext > 0 && normal[1] > 0) {
546 } else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0 || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) {
552 x = cur[0] + normal[0] * offsetFromViz;
553 y = cur[1] + normal[1] * offsetFromViz;
555 //box position and dimensions
556 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
557 boxy = y - bbox.height /2 - offsetBox;
558 boxw = bbox.width + 2 * offsetBox;
559 boxh = bbox.height + 2 * offsetBox;
561 //now check if we're out of bounds and invert the normal vector correspondingly
562 //this may add new overlaps between labels (but labels won't be out of bounds).
563 if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) {
566 if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) {
571 x = cur[0] + normal[0] * offsetFromViz;
572 y = cur[1] + normal[1] * offsetFromViz;
574 //update box position and dimensions
575 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
576 boxy = y - bbox.height /2 - offsetBox;
577 boxw = bbox.width + 2 * offsetBox;
578 boxh = bbox.height + 2 * offsetBox;
580 //set the line from the middle of the pie to the box.
581 callout.lines.setAttributes({
582 path: ["M", cur[0], cur[1], "L", x, y, "Z"]
585 callout.box.setAttributes({
592 callout.label.setAttributes({
593 x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)),
597 callout[p].show(true);
601 isItemInPoint: function(x, y, item, i) {
603 pointsUp = item.pointsUp,
604 pointsDown = item.pointsDown,
606 dist = Infinity, p, pln, point;
608 for (p = 0, pln = pointsUp.length; p < pln; p++) {
609 point = [pointsUp[p][0], pointsUp[p][1]];
610 if (dist > abs(x - point[0])) {
611 dist = abs(x - point[0]);
613 point = pointsUp[p -1];
614 if (y >= point[1] && (!pointsDown.length || y <= (pointsDown[p -1][1]))) {
615 item.storeIndex = p -1;
616 item.storeField = me.yField[i];
617 item.storeItem = me.chart.store.getAt(p -1);
618 item._points = pointsDown.length? [point, pointsDown[p -1]] : [point];
629 * Highlight this entire series.
630 * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
632 highlightSeries: function() {
633 var area, to, fillColor;
634 if (this._index !== undefined) {
635 area = this.areas[this._index];
636 if (area.__highlightAnim) {
637 area.__highlightAnim.paused = true;
639 area.__highlighted = true;
640 area.__prevOpacity = area.__prevOpacity || area.attr.opacity || 1;
641 area.__prevFill = area.__prevFill || area.attr.fill;
642 area.__prevLineWidth = area.__prevLineWidth || area.attr.lineWidth;
643 fillColor = Ext.draw.Color.fromString(area.__prevFill);
645 lineWidth: (area.__prevLineWidth || 0) + 2
648 to.fill = fillColor.getLighter(0.2).toString();
651 to.opacity = Math.max(area.__prevOpacity - 0.3, 0);
653 if (this.chart.animate) {
654 area.__highlightAnim = Ext.create('Ext.fx.Anim', Ext.apply({
657 }, this.chart.animate));
660 area.setAttributes(to, true);
666 * UnHighlight this entire series.
667 * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
669 unHighlightSeries: function() {
671 if (this._index !== undefined) {
672 area = this.areas[this._index];
673 if (area.__highlightAnim) {
674 area.__highlightAnim.paused = true;
676 if (area.__highlighted) {
677 area.__highlighted = false;
678 area.__highlightAnim = Ext.create('Ext.fx.Anim', {
681 fill: area.__prevFill,
682 opacity: area.__prevOpacity,
683 lineWidth: area.__prevLineWidth
691 * Highlight the specified item. If no item is provided the whole series will be highlighted.
692 * @param item {Object} Info about the item; same format as returned by #getItemForPoint
694 highlightItem: function(item) {
698 this.highlightSeries();
701 points = item._points;
702 path = points.length == 2? ['M', points[0][0], points[0][1], 'L', points[1][0], points[1][1]]
703 : ['M', points[0][0], points[0][1], 'L', points[0][0], me.bbox.y + me.bbox.height];
704 me.highlightSprite.setAttributes({
711 * un-highlights the specified item. If no item is provided it will un-highlight the entire series.
712 * @param item {Object} Info about the item; same format as returned by #getItemForPoint
714 unHighlightItem: function(item) {
716 this.unHighlightSeries();
719 if (this.highlightSprite) {
720 this.highlightSprite.hide(true);
725 hideAll: function() {
726 if (!isNaN(this._index)) {
727 this.__excludes[this._index] = true;
728 this.areas[this._index].hide(true);
734 showAll: function() {
735 if (!isNaN(this._index)) {
736 this.__excludes[this._index] = false;
737 this.areas[this._index].show(true);
743 * Returns the color of the series (to be displayed as color for the series legend item).
744 * @param item {Object} Info about the item; same format as returned by #getItemForPoint
746 getLegendColor: function(index) {
748 return me.colorArrayStyle[index % me.colorArrayStyle.length];