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
19 * Creates a Stacked Area Chart. The stacked area chart is useful when displaying multiple aggregated layers of information.
20 * As with all other series, the Area Series must be appended in the *series* Chart array configuration. See the Chart
21 * documentation for more information. A typical configuration object for the area series could be:
24 * var store = Ext.create('Ext.data.JsonStore', {
25 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
27 * { 'name': 'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13 },
28 * { 'name': 'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3 },
29 * { 'name': 'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7 },
30 * { 'name': 'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23 },
31 * { 'name': 'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33 }
35 * Ext.create('Ext.chart.Chart', {
36 * renderTo: Ext.getBody(),
45 * fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
46 * title: 'Sample Values',
56 * adjustMinimumByMajorUnit: 0
62 * title: 'Sample Metrics',
76 * yField: ['data1', 'data2', 'data3', 'data4', 'data5'],
83 * In this configuration we set `area` as the type for the series, set highlighting options to true for highlighting elements on hover,
84 * 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,
85 * and as yFields (aggregated layers) seven data fields from the same store. Then we override some theming styles by adding some opacity
86 * to the style object.
90 Ext.define('Ext.chart.series.Area', {
92 /* Begin Definitions */
94 extend: 'Ext.chart.series.Cartesian',
98 requires: ['Ext.chart.axis.Axis', 'Ext.draw.Color', 'Ext.fx.Anim'],
100 /* End Definitions */
104 // @private Area charts are alyways stacked
108 * @cfg {Object} style
109 * Append styling properties to this object for it to override theme properties.
113 constructor: function(config) {
114 this.callParent(arguments);
116 surface = me.chart.surface,
118 Ext.apply(me, config, {
128 me.highlightSprite = surface.add({
138 me.group = surface.getGroup(me.seriesId);
141 // @private Shrinks dataSets down to a smaller size
142 shrink: function(xValues, yValues, size) {
143 var len = xValues.length,
144 ratio = Math.floor(len / size),
147 yCompLen = this.areas.length,
152 for (j = 0; j < yCompLen; ++j) {
155 for (i = 0; i < len; ++i) {
157 for (j = 0; j < yCompLen; ++j) {
158 ySum[j] += yValues[i][j];
160 if (i % ratio == 0) {
162 xRes.push(xSum/ratio);
163 for (j = 0; j < yCompLen; ++j) {
167 //reset sum accumulators
169 for (j = 0, ySum = []; j < yCompLen; ++j) {
180 // @private Get chart and data boundaries
181 getBounds: function() {
184 store = chart.getChartStore(),
185 areas = [].concat(me.yField),
186 areasLen = areas.length,
197 bbox, xScale, yScale, xValue, yValue, areaIndex, acumY, ln, sumValues, clipBox, areaElem;
202 // Run through the axis
204 axis = chart.axes.get(me.axis);
206 out = axis.calcEnds();
207 minY = out.from || axis.prevMin;
208 maxY = mmax(out.to || axis.prevMax, 0);
212 if (me.yField && !Ext.isNumber(minY)) {
213 axis = Ext.create('Ext.chart.axis.Axis', {
215 fields: [].concat(me.yField)
217 out = axis.calcEnds();
218 minY = out.from || axis.prevMin;
219 maxY = mmax(out.to || axis.prevMax, 0);
222 if (!Ext.isNumber(minY)) {
225 if (!Ext.isNumber(maxY)) {
229 store.each(function(record, i) {
230 xValue = record.get(me.xField);
232 if (typeof xValue != 'number') {
235 xValues.push(xValue);
237 for (areaIndex = 0; areaIndex < areasLen; areaIndex++) {
238 areaElem = record.get(areas[areaIndex]);
239 if (typeof areaElem == 'number') {
240 minY = mmin(minY, areaElem);
241 yValue.push(areaElem);
245 minX = mmin(minX, xValue);
246 maxX = mmax(maxX, xValue);
247 maxY = mmax(maxY, acumY);
248 yValues.push(yValue);
251 xScale = bbox.width / ((maxX - minX) || 1);
252 yScale = bbox.height / ((maxY - minY) || 1);
255 if ((ln > bbox.width) && me.areas) {
256 sumValues = me.shrink(xValues, yValues, bbox.width);
257 xValues = sumValues.x;
258 yValues = sumValues.y;
273 // @private Build an array of paths for the chart
274 getPaths: function() {
277 store = chart.getChartStore(),
279 bounds = me.getBounds(),
281 items = me.items = [],
285 i, ln, x, y, xValue, yValue, acumY, areaIndex, prevAreaIndex, areaElem, path;
287 ln = bounds.xValues.length;
289 for (i = 0; i < ln; i++) {
290 xValue = bounds.xValues[i];
291 yValue = bounds.yValues[i];
292 x = bbox.x + (xValue - bounds.minX) * bounds.xScale;
294 for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
296 if (me.__excludes[areaIndex]) {
299 if (!componentPaths[areaIndex]) {
300 componentPaths[areaIndex] = [];
302 areaElem = yValue[areaIndex];
304 y = bbox.y + bbox.height - (acumY - bounds.minY) * bounds.yScale;
305 if (!paths[areaIndex]) {
306 paths[areaIndex] = ['M', x, y];
307 componentPaths[areaIndex].push(['L', x, y]);
309 paths[areaIndex].push('L', x, y);
310 componentPaths[areaIndex].push(['L', x, y]);
312 if (!items[areaIndex]) {
319 items[areaIndex].pointsUp.push([x, y]);
324 for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
326 if (me.__excludes[areaIndex]) {
329 path = paths[areaIndex];
330 // Close bottom path to the axis
331 if (areaIndex == 0 || first) {
333 path.push('L', x, bbox.y + bbox.height,
334 'L', bbox.x, bbox.y + bbox.height,
337 // Close other paths to the one before them
339 componentPath = componentPaths[prevAreaIndex];
340 componentPath.reverse();
341 path.push('L', x, componentPath[0][2]);
342 for (i = 0; i < ln; i++) {
343 path.push(componentPath[i][0],
345 componentPath[i][2]);
346 items[areaIndex].pointsDown[ln -i -1] = [componentPath[i][1], componentPath[i][2]];
348 path.push('L', bbox.x, path[2], 'Z');
350 prevAreaIndex = areaIndex;
354 areasLen: bounds.areasLen
359 * Draws the series for the current chart.
361 drawSeries: function() {
364 store = chart.getChartStore(),
365 surface = chart.surface,
366 animate = chart.animate,
368 endLineStyle = Ext.apply(me.seriesStyle, me.style),
369 colorArrayStyle = me.colorArrayStyle,
370 colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
371 areaIndex, areaElem, paths, path, rendererAttributes;
373 me.unHighlightItem();
374 me.cleanHighlights();
376 if (!store || !store.getCount()) {
380 paths = me.getPaths();
386 for (areaIndex = 0; areaIndex < paths.areasLen; areaIndex++) {
388 if (me.__excludes[areaIndex]) {
391 if (!me.areas[areaIndex]) {
392 me.items[areaIndex].sprite = me.areas[areaIndex] = surface.add(Ext.apply({}, {
395 // 'clip-rect': me.clipBox,
396 path: paths.paths[areaIndex],
397 stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength],
398 fill: colorArrayStyle[areaIndex % colorArrayLength]
399 }, endLineStyle || {}));
401 areaElem = me.areas[areaIndex];
402 path = paths.paths[areaIndex];
404 //Add renderer to line. There is not a unique record associated with this.
405 rendererAttributes = me.renderer(areaElem, false, {
407 // 'clip-rect': me.clipBox,
408 fill: colorArrayStyle[areaIndex % colorArrayLength],
409 stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength]
410 }, areaIndex, store);
411 //fill should not be used here but when drawing the special fill path object
412 me.animation = me.onAnimate(areaElem, {
413 to: rendererAttributes
416 rendererAttributes = me.renderer(areaElem, false, {
418 // 'clip-rect': me.clipBox,
420 fill: colorArrayStyle[areaIndex % colorArrayLength],
421 stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength]
422 }, areaIndex, store);
423 me.areas[areaIndex].setAttributes(rendererAttributes, true);
431 onAnimate: function(sprite, attr) {
433 return this.callParent(arguments);
437 onCreateLabel: function(storeItem, item, i, display) {
439 group = me.labelsGroup,
442 endLabelStyle = Ext.apply(config, me.seriesLabelStyle);
444 return me.chart.surface.add(Ext.apply({
446 'text-anchor': 'middle',
449 'y': bbox.y + bbox.height / 2
450 }, endLabelStyle || {}));
454 onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {
457 resizing = chart.resizing,
459 format = config.renderer,
460 field = config.field,
466 label.setAttributes({
467 text: format(storeItem.get(field[index])),
471 bb = label.getBBox();
472 width = bb.width / 2;
473 height = bb.height / 2;
475 x = x - width < bbox.x? bbox.x + width : x;
476 x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
477 y = y - height < bbox.y? bbox.y + height : y;
478 y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
480 if (me.chart.animate && !me.chart.resizing) {
482 me.onAnimate(label, {
489 label.setAttributes({
494 me.animation.on('afteranimate', function() {
504 onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) {
507 surface = chart.surface,
508 resizing = chart.resizing,
509 config = me.callouts,
511 prev = (i == 0) ? false : items[i -1].point,
512 next = (i == items.length -1) ? false : items[i +1].point,
514 dir, norm, normal, a, aprev, anext,
515 bbox = callout.label.getBBox(),
519 boxx, boxy, boxw, boxh,
520 p, clipRect = me.clipRect,
523 //get the right two points
530 a = (next[1] - prev[1]) / (next[0] - prev[0]);
531 aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]);
532 anext = (next[1] - cur[1]) / (next[0] - cur[0]);
534 norm = Math.sqrt(1 + a * a);
535 dir = [1 / norm, a / norm];
536 normal = [-dir[1], dir[0]];
538 //keep the label always on the outer part of the "elbow"
539 if (aprev > 0 && anext < 0 && normal[1] < 0 || aprev < 0 && anext > 0 && normal[1] > 0) {
542 } else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0 || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) {
548 x = cur[0] + normal[0] * offsetFromViz;
549 y = cur[1] + normal[1] * offsetFromViz;
551 //box position and dimensions
552 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
553 boxy = y - bbox.height /2 - offsetBox;
554 boxw = bbox.width + 2 * offsetBox;
555 boxh = bbox.height + 2 * offsetBox;
557 //now check if we're out of bounds and invert the normal vector correspondingly
558 //this may add new overlaps between labels (but labels won't be out of bounds).
559 if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) {
562 if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) {
567 x = cur[0] + normal[0] * offsetFromViz;
568 y = cur[1] + normal[1] * offsetFromViz;
570 //update box position and dimensions
571 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
572 boxy = y - bbox.height /2 - offsetBox;
573 boxw = bbox.width + 2 * offsetBox;
574 boxh = bbox.height + 2 * offsetBox;
576 //set the line from the middle of the pie to the box.
577 callout.lines.setAttributes({
578 path: ["M", cur[0], cur[1], "L", x, y, "Z"]
581 callout.box.setAttributes({
588 callout.label.setAttributes({
589 x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)),
593 callout[p].show(true);
597 isItemInPoint: function(x, y, item, i) {
599 pointsUp = item.pointsUp,
600 pointsDown = item.pointsDown,
602 dist = Infinity, p, pln, point;
604 for (p = 0, pln = pointsUp.length; p < pln; p++) {
605 point = [pointsUp[p][0], pointsUp[p][1]];
606 if (dist > abs(x - point[0])) {
607 dist = abs(x - point[0]);
609 point = pointsUp[p -1];
610 if (y >= point[1] && (!pointsDown.length || y <= (pointsDown[p -1][1]))) {
611 item.storeIndex = p -1;
612 item.storeField = me.yField[i];
613 item.storeItem = me.chart.store.getAt(p -1);
614 item._points = pointsDown.length? [point, pointsDown[p -1]] : [point];
625 * Highlight this entire series.
626 * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
628 highlightSeries: function() {
629 var area, to, fillColor;
630 if (this._index !== undefined) {
631 area = this.areas[this._index];
632 if (area.__highlightAnim) {
633 area.__highlightAnim.paused = true;
635 area.__highlighted = true;
636 area.__prevOpacity = area.__prevOpacity || area.attr.opacity || 1;
637 area.__prevFill = area.__prevFill || area.attr.fill;
638 area.__prevLineWidth = area.__prevLineWidth || area.attr.lineWidth;
639 fillColor = Ext.draw.Color.fromString(area.__prevFill);
641 lineWidth: (area.__prevLineWidth || 0) + 2
644 to.fill = fillColor.getLighter(0.2).toString();
647 to.opacity = Math.max(area.__prevOpacity - 0.3, 0);
649 if (this.chart.animate) {
650 area.__highlightAnim = Ext.create('Ext.fx.Anim', Ext.apply({
653 }, this.chart.animate));
656 area.setAttributes(to, true);
662 * UnHighlight this entire series.
663 * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
665 unHighlightSeries: function() {
667 if (this._index !== undefined) {
668 area = this.areas[this._index];
669 if (area.__highlightAnim) {
670 area.__highlightAnim.paused = true;
672 if (area.__highlighted) {
673 area.__highlighted = false;
674 area.__highlightAnim = Ext.create('Ext.fx.Anim', {
677 fill: area.__prevFill,
678 opacity: area.__prevOpacity,
679 lineWidth: area.__prevLineWidth
687 * Highlight the specified item. If no item is provided the whole series will be highlighted.
688 * @param item {Object} Info about the item; same format as returned by #getItemForPoint
690 highlightItem: function(item) {
694 this.highlightSeries();
697 points = item._points;
698 path = points.length == 2? ['M', points[0][0], points[0][1], 'L', points[1][0], points[1][1]]
699 : ['M', points[0][0], points[0][1], 'L', points[0][0], me.bbox.y + me.bbox.height];
700 me.highlightSprite.setAttributes({
707 * Un-highlights the specified item. If no item is provided it will un-highlight the entire series.
708 * @param {Object} item Info about the item; same format as returned by #getItemForPoint
710 unHighlightItem: function(item) {
712 this.unHighlightSeries();
715 if (this.highlightSprite) {
716 this.highlightSprite.hide(true);
721 hideAll: function() {
722 if (!isNaN(this._index)) {
723 this.__excludes[this._index] = true;
724 this.areas[this._index].hide(true);
730 showAll: function() {
731 if (!isNaN(this._index)) {
732 this.__excludes[this._index] = false;
733 this.areas[this._index].show(true);
739 * Returns the color of the series (to be displayed as color for the series legend item).
740 * @param item {Object} Info about the item; same format as returned by #getItemForPoint
742 getLegendColor: function(index) {
744 return me.colorArrayStyle[index % me.colorArrayStyle.length];
748 * @class Ext.chart.series.Area
749 * @extends Ext.chart.series.Cartesian
751 * Creates a Stacked Area Chart. The stacked area chart is useful when displaying multiple aggregated layers of information.
752 * As with all other series, the Area Series must be appended in the *series* Chart array configuration. See the Chart
753 * documentation for more information. A typical configuration object for the area series could be:
756 * var store = Ext.create('Ext.data.JsonStore', {
757 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
759 * { 'name': 'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13 },
760 * { 'name': 'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3 },
761 * { 'name': 'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7 },
762 * { 'name': 'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23 },
763 * { 'name': 'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33 }
767 * Ext.create('Ext.chart.Chart', {
768 * renderTo: Ext.getBody(),
777 * fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
778 * title: 'Sample Values',
788 * adjustMinimumByMajorUnit: 0
792 * position: 'bottom',
794 * title: 'Sample Metrics',
808 * yField: ['data1', 'data2', 'data3', 'data4', 'data5'],
815 * In this configuration we set `area` as the type for the series, set highlighting options to true for highlighting elements on hover,
816 * 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,
817 * and as yFields (aggregated layers) seven data fields from the same store. Then we override some theming styles by adding some opacity
818 * to the style object.
822 Ext.define('Ext.chart.series.Area', {
824 /* Begin Definitions */
826 extend: 'Ext.chart.series.Cartesian',
828 alias: 'series.area',
830 requires: ['Ext.chart.axis.Axis', 'Ext.draw.Color', 'Ext.fx.Anim'],
832 /* End Definitions */
836 // @private Area charts are alyways stacked
840 * @cfg {Object} style
841 * Append styling properties to this object for it to override theme properties.
845 constructor: function(config) {
846 this.callParent(arguments);
848 surface = me.chart.surface,
850 Ext.apply(me, config, {
860 me.highlightSprite = surface.add({
870 me.group = surface.getGroup(me.seriesId);
873 // @private Shrinks dataSets down to a smaller size
874 shrink: function(xValues, yValues, size) {
875 var len = xValues.length,
876 ratio = Math.floor(len / size),
879 yCompLen = this.areas.length,
884 for (j = 0; j < yCompLen; ++j) {
887 for (i = 0; i < len; ++i) {
889 for (j = 0; j < yCompLen; ++j) {
890 ySum[j] += yValues[i][j];
892 if (i % ratio == 0) {
894 xRes.push(xSum/ratio);
895 for (j = 0; j < yCompLen; ++j) {
899 //reset sum accumulators
901 for (j = 0, ySum = []; j < yCompLen; ++j) {
912 // @private Get chart and data boundaries
913 getBounds: function() {
916 store = chart.getChartStore(),
917 areas = [].concat(me.yField),
918 areasLen = areas.length,
929 bbox, xScale, yScale, xValue, yValue, areaIndex, acumY, ln, sumValues, clipBox, areaElem;
934 // Run through the axis
936 axis = chart.axes.get(me.axis);
938 out = axis.calcEnds();
939 minY = out.from || axis.prevMin;
940 maxY = mmax(out.to || axis.prevMax, 0);
944 if (me.yField && !Ext.isNumber(minY)) {
945 axis = Ext.create('Ext.chart.axis.Axis', {
947 fields: [].concat(me.yField)
949 out = axis.calcEnds();
950 minY = out.from || axis.prevMin;
951 maxY = mmax(out.to || axis.prevMax, 0);
954 if (!Ext.isNumber(minY)) {
957 if (!Ext.isNumber(maxY)) {
961 store.each(function(record, i) {
962 xValue = record.get(me.xField);
964 if (typeof xValue != 'number') {
967 xValues.push(xValue);
969 for (areaIndex = 0; areaIndex < areasLen; areaIndex++) {
970 areaElem = record.get(areas[areaIndex]);
971 if (typeof areaElem == 'number') {
972 minY = mmin(minY, areaElem);
973 yValue.push(areaElem);
977 minX = mmin(minX, xValue);
978 maxX = mmax(maxX, xValue);
979 maxY = mmax(maxY, acumY);
980 yValues.push(yValue);
983 xScale = bbox.width / ((maxX - minX) || 1);
984 yScale = bbox.height / ((maxY - minY) || 1);
987 if ((ln > bbox.width) && me.areas) {
988 sumValues = me.shrink(xValues, yValues, bbox.width);
989 xValues = sumValues.x;
990 yValues = sumValues.y;
1005 // @private Build an array of paths for the chart
1006 getPaths: function() {
1009 store = chart.getChartStore(),
1011 bounds = me.getBounds(),
1013 items = me.items = [],
1014 componentPaths = [],
1017 i, ln, x, y, xValue, yValue, acumY, areaIndex, prevAreaIndex, areaElem, path;
1019 ln = bounds.xValues.length;
1021 for (i = 0; i < ln; i++) {
1022 xValue = bounds.xValues[i];
1023 yValue = bounds.yValues[i];
1024 x = bbox.x + (xValue - bounds.minX) * bounds.xScale;
1026 for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
1028 if (me.__excludes[areaIndex]) {
1031 if (!componentPaths[areaIndex]) {
1032 componentPaths[areaIndex] = [];
1034 areaElem = yValue[areaIndex];
1036 y = bbox.y + bbox.height - (acumY - bounds.minY) * bounds.yScale;
1037 if (!paths[areaIndex]) {
1038 paths[areaIndex] = ['M', x, y];
1039 componentPaths[areaIndex].push(['L', x, y]);
1041 paths[areaIndex].push('L', x, y);
1042 componentPaths[areaIndex].push(['L', x, y]);
1044 if (!items[areaIndex]) {
1045 items[areaIndex] = {
1051 items[areaIndex].pointsUp.push([x, y]);
1056 for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
1058 if (me.__excludes[areaIndex]) {
1061 path = paths[areaIndex];
1062 // Close bottom path to the axis
1063 if (areaIndex == 0 || first) {
1065 path.push('L', x, bbox.y + bbox.height,
1066 'L', bbox.x, bbox.y + bbox.height,
1069 // Close other paths to the one before them
1071 componentPath = componentPaths[prevAreaIndex];
1072 componentPath.reverse();
1073 path.push('L', x, componentPath[0][2]);
1074 for (i = 0; i < ln; i++) {
1075 path.push(componentPath[i][0],
1076 componentPath[i][1],
1077 componentPath[i][2]);
1078 items[areaIndex].pointsDown[ln -i -1] = [componentPath[i][1], componentPath[i][2]];
1080 path.push('L', bbox.x, path[2], 'Z');
1082 prevAreaIndex = areaIndex;
1086 areasLen: bounds.areasLen
1091 * Draws the series for the current chart.
1093 drawSeries: function() {
1096 store = chart.getChartStore(),
1097 surface = chart.surface,
1098 animate = chart.animate,
1100 endLineStyle = Ext.apply(me.seriesStyle, me.style),
1101 colorArrayStyle = me.colorArrayStyle,
1102 colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
1103 areaIndex, areaElem, paths, path, rendererAttributes;
1105 me.unHighlightItem();
1106 me.cleanHighlights();
1108 if (!store || !store.getCount()) {
1112 paths = me.getPaths();
1118 for (areaIndex = 0; areaIndex < paths.areasLen; areaIndex++) {
1120 if (me.__excludes[areaIndex]) {
1123 if (!me.areas[areaIndex]) {
1124 me.items[areaIndex].sprite = me.areas[areaIndex] = surface.add(Ext.apply({}, {
1127 // 'clip-rect': me.clipBox,
1128 path: paths.paths[areaIndex],
1129 stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength],
1130 fill: colorArrayStyle[areaIndex % colorArrayLength]
1131 }, endLineStyle || {}));
1133 areaElem = me.areas[areaIndex];
1134 path = paths.paths[areaIndex];
1136 //Add renderer to line. There is not a unique record associated with this.
1137 rendererAttributes = me.renderer(areaElem, false, {
1139 // 'clip-rect': me.clipBox,
1140 fill: colorArrayStyle[areaIndex % colorArrayLength],
1141 stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength]
1142 }, areaIndex, store);
1143 //fill should not be used here but when drawing the special fill path object
1144 me.animation = me.onAnimate(areaElem, {
1145 to: rendererAttributes
1148 rendererAttributes = me.renderer(areaElem, false, {
1150 // 'clip-rect': me.clipBox,
1152 fill: colorArrayStyle[areaIndex % colorArrayLength],
1153 stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength]
1154 }, areaIndex, store);
1155 me.areas[areaIndex].setAttributes(rendererAttributes, true);
1159 me.renderCallouts();
1163 onAnimate: function(sprite, attr) {
1165 return this.callParent(arguments);
1169 onCreateLabel: function(storeItem, item, i, display) {
1171 group = me.labelsGroup,
1174 endLabelStyle = Ext.apply(config, me.seriesLabelStyle);
1176 return me.chart.surface.add(Ext.apply({
1178 'text-anchor': 'middle',
1181 'y': bbox.y + bbox.height / 2
1182 }, endLabelStyle || {}));
1186 onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {
1189 resizing = chart.resizing,
1191 format = config.renderer,
1192 field = config.field,
1198 label.setAttributes({
1199 text: format(storeItem.get(field[index])),
1203 bb = label.getBBox();
1204 width = bb.width / 2;
1205 height = bb.height / 2;
1207 x = x - width < bbox.x? bbox.x + width : x;
1208 x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
1209 y = y - height < bbox.y? bbox.y + height : y;
1210 y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
1212 if (me.chart.animate && !me.chart.resizing) {
1214 me.onAnimate(label, {
1221 label.setAttributes({
1226 me.animation.on('afteranimate', function() {
1236 onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) {
1239 surface = chart.surface,
1240 resizing = chart.resizing,
1241 config = me.callouts,
1243 prev = (i == 0) ? false : items[i -1].point,
1244 next = (i == items.length -1) ? false : items[i +1].point,
1246 dir, norm, normal, a, aprev, anext,
1247 bbox = callout.label.getBBox(),
1251 boxx, boxy, boxw, boxh,
1252 p, clipRect = me.clipRect,
1255 //get the right two points
1262 a = (next[1] - prev[1]) / (next[0] - prev[0]);
1263 aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]);
1264 anext = (next[1] - cur[1]) / (next[0] - cur[0]);
1266 norm = Math.sqrt(1 + a * a);
1267 dir = [1 / norm, a / norm];
1268 normal = [-dir[1], dir[0]];
1270 //keep the label always on the outer part of the "elbow"
1271 if (aprev > 0 && anext < 0 && normal[1] < 0 || aprev < 0 && anext > 0 && normal[1] > 0) {
1274 } else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0 || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) {
1280 x = cur[0] + normal[0] * offsetFromViz;
1281 y = cur[1] + normal[1] * offsetFromViz;
1283 //box position and dimensions
1284 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
1285 boxy = y - bbox.height /2 - offsetBox;
1286 boxw = bbox.width + 2 * offsetBox;
1287 boxh = bbox.height + 2 * offsetBox;
1289 //now check if we're out of bounds and invert the normal vector correspondingly
1290 //this may add new overlaps between labels (but labels won't be out of bounds).
1291 if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) {
1294 if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) {
1299 x = cur[0] + normal[0] * offsetFromViz;
1300 y = cur[1] + normal[1] * offsetFromViz;
1302 //update box position and dimensions
1303 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
1304 boxy = y - bbox.height /2 - offsetBox;
1305 boxw = bbox.width + 2 * offsetBox;
1306 boxh = bbox.height + 2 * offsetBox;
1308 //set the line from the middle of the pie to the box.
1309 callout.lines.setAttributes({
1310 path: ["M", cur[0], cur[1], "L", x, y, "Z"]
1313 callout.box.setAttributes({
1320 callout.label.setAttributes({
1321 x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)),
1324 for (p in callout) {
1325 callout[p].show(true);
1329 isItemInPoint: function(x, y, item, i) {
1331 pointsUp = item.pointsUp,
1332 pointsDown = item.pointsDown,
1334 dist = Infinity, p, pln, point;
1336 for (p = 0, pln = pointsUp.length; p < pln; p++) {
1337 point = [pointsUp[p][0], pointsUp[p][1]];
1338 if (dist > abs(x - point[0])) {
1339 dist = abs(x - point[0]);
1341 point = pointsUp[p -1];
1342 if (y >= point[1] && (!pointsDown.length || y <= (pointsDown[p -1][1]))) {
1343 item.storeIndex = p -1;
1344 item.storeField = me.yField[i];
1345 item.storeItem = me.chart.store.getAt(p -1);
1346 item._points = pointsDown.length? [point, pointsDown[p -1]] : [point];
1357 * Highlight this entire series.
1358 * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
1360 highlightSeries: function() {
1361 var area, to, fillColor;
1362 if (this._index !== undefined) {
1363 area = this.areas[this._index];
1364 if (area.__highlightAnim) {
1365 area.__highlightAnim.paused = true;
1367 area.__highlighted = true;
1368 area.__prevOpacity = area.__prevOpacity || area.attr.opacity || 1;
1369 area.__prevFill = area.__prevFill || area.attr.fill;
1370 area.__prevLineWidth = area.__prevLineWidth || area.attr.lineWidth;
1371 fillColor = Ext.draw.Color.fromString(area.__prevFill);
1373 lineWidth: (area.__prevLineWidth || 0) + 2
1376 to.fill = fillColor.getLighter(0.2).toString();
1379 to.opacity = Math.max(area.__prevOpacity - 0.3, 0);
1381 if (this.chart.animate) {
1382 area.__highlightAnim = Ext.create('Ext.fx.Anim', Ext.apply({
1385 }, this.chart.animate));
1388 area.setAttributes(to, true);
1394 * UnHighlight this entire series.
1395 * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
1397 unHighlightSeries: function() {
1399 if (this._index !== undefined) {
1400 area = this.areas[this._index];
1401 if (area.__highlightAnim) {
1402 area.__highlightAnim.paused = true;
1404 if (area.__highlighted) {
1405 area.__highlighted = false;
1406 area.__highlightAnim = Ext.create('Ext.fx.Anim', {
1409 fill: area.__prevFill,
1410 opacity: area.__prevOpacity,
1411 lineWidth: area.__prevLineWidth
1419 * Highlight the specified item. If no item is provided the whole series will be highlighted.
1420 * @param item {Object} Info about the item; same format as returned by #getItemForPoint
1422 highlightItem: function(item) {
1426 this.highlightSeries();
1429 points = item._points;
1430 path = points.length == 2? ['M', points[0][0], points[0][1], 'L', points[1][0], points[1][1]]
1431 : ['M', points[0][0], points[0][1], 'L', points[0][0], me.bbox.y + me.bbox.height];
1432 me.highlightSprite.setAttributes({
1439 * un-highlights the specified item. If no item is provided it will un-highlight the entire series.
1440 * @param item {Object} Info about the item; same format as returned by #getItemForPoint
1442 unHighlightItem: function(item) {
1444 this.unHighlightSeries();
1447 if (this.highlightSprite) {
1448 this.highlightSprite.hide(true);
1453 hideAll: function() {
1454 if (!isNaN(this._index)) {
1455 this.__excludes[this._index] = true;
1456 this.areas[this._index].hide(true);
1462 showAll: function() {
1463 if (!isNaN(this._index)) {
1464 this.__excludes[this._index] = false;
1465 this.areas[this._index].show(true);
1471 * Returns the color of the series (to be displayed as color for the series legend item).
1472 * @param item {Object} Info about the item; same format as returned by #getItemForPoint
1474 getLegendColor: function(index) {
1476 return me.colorArrayStyle[index % me.colorArrayStyle.length];