Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / src / chart / series / Line.js
1 /**
2  * @class Ext.chart.series.Line
3  * @extends Ext.chart.series.Cartesian
4  * 
5  * Creates a Line Chart. A Line Chart is a useful visualization technique to display quantitative information for different 
6  * categories or other real values (as opposed to the bar chart), that can show some progression (or regression) in the dataset.
7  * As with all other series, the Line Series must be appended in the *series* Chart array configuration. See the Chart 
8  * documentation for more information. A typical configuration object for the line series could be:
9  *
10  * {@img Ext.chart.series.Line/Ext.chart.series.Line.png Ext.chart.series.Line chart series}
11  *
12  *     var store = Ext.create('Ext.data.JsonStore', {
13  *         fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
14  *         data: [
15  *             {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
16  *             {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
17  *             {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
18  *             {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
19  *             {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}                                                
20  *         ]
21  *     });
22  *     
23  *     Ext.create('Ext.chart.Chart', {
24  *         renderTo: Ext.getBody(),
25  *         width: 500,
26  *         height: 300,
27  *         animate: true,
28  *         store: store,
29  *         axes: [{
30  *             type: 'Numeric',
31  *             position: 'bottom',
32  *             fields: ['data1'],
33  *             label: {
34  *                 renderer: Ext.util.Format.numberRenderer('0,0')
35  *             },
36  *             title: 'Sample Values',
37  *             grid: true,
38  *             minimum: 0
39  *         }, {
40  *             type: 'Category',
41  *             position: 'left',
42  *             fields: ['name'],
43  *             title: 'Sample Metrics'
44  *         }],
45  *         series: [{
46  *             type: 'line',
47  *             highlight: {
48  *                 size: 7,
49  *                 radius: 7
50  *             },
51  *             axis: 'left',
52  *             xField: 'name',
53  *             yField: 'data1',
54  *             markerCfg: {
55  *                 type: 'cross',
56  *                 size: 4,
57  *                 radius: 4,
58  *                 'stroke-width': 0
59  *             }
60  *         }, {
61  *             type: 'line',
62  *             highlight: {
63  *                 size: 7,
64  *                 radius: 7
65  *             },
66  *             axis: 'left',
67  *             fill: true,
68  *             xField: 'name',
69  *             yField: 'data3',
70  *             markerCfg: {
71  *                 type: 'circle',
72  *                 size: 4,
73  *                 radius: 4,
74  *                 'stroke-width': 0
75  *             }
76  *         }]
77  *     });
78  *  
79  * In this configuration we're adding two series (or lines), one bound to the `data1` 
80  * property of the store and the other to `data3`. The type for both configurations is 
81  * `line`. The `xField` for both series is the same, the name propert of the store. 
82  * Both line series share the same axis, the left axis. You can set particular marker 
83  * configuration by adding properties onto the markerConfig object. Both series have 
84  * an object as highlight so that markers animate smoothly to the properties in highlight 
85  * when hovered. The second series has `fill=true` which means that the line will also 
86  * have an area below it of the same color.
87  *
88  * **Note:** In the series definition remember to explicitly set the axis to bind the 
89  * values of the line series to. This can be done by using the `axis` configuration property.
90  */
91 Ext.define('Ext.chart.series.Line', {
92
93     /* Begin Definitions */
94
95     extend: 'Ext.chart.series.Cartesian',
96
97     alternateClassName: ['Ext.chart.LineSeries', 'Ext.chart.LineChart'],
98
99     requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shape', 'Ext.draw.Draw', 'Ext.fx.Anim'],
100
101     /* End Definitions */
102
103     type: 'line',
104     
105     alias: 'series.line',
106     
107     /**
108      * @cfg {String} axis
109      * The position of the axis to bind the values to. Possible values are 'left', 'bottom', 'top' and 'right'.
110      * You must explicitly set this value to bind the values of the line series to the ones in the axis, otherwise a
111      * relative scale will be used.
112      */
113
114     /**
115      * @cfg {Number} selectionTolerance
116      * The offset distance from the cursor position to the line series to trigger events (then used for highlighting series, etc).
117      */
118     selectionTolerance: 20,
119     
120     /**
121      * @cfg {Boolean} showMarkers
122      * Whether markers should be displayed at the data points along the line. If true,
123      * then the {@link #markerConfig} config item will determine the markers' styling.
124      */
125     showMarkers: true,
126
127     /**
128      * @cfg {Object} markerConfig
129      * The display style for the markers. Only used if {@link #showMarkers} is true.
130      * The markerConfig is a configuration object containing the same set of properties defined in
131      * the Sprite class. For example, if we were to set red circles as markers to the line series we could
132      * pass the object:
133      *
134      <pre><code>
135         markerConfig: {
136             type: 'circle',
137             radius: 4,
138             'fill': '#f00'
139         }
140      </code></pre>
141      
142      */
143     markerConfig: {},
144
145     /**
146      * @cfg {Object} style
147      * An object containing styles for the visualization lines. These styles will override the theme styles. 
148      * Some options contained within the style object will are described next.
149      */
150     style: {},
151     
152     /**
153      * @cfg {Boolean} smooth
154      * If true, the line will be smoothed/rounded around its points, otherwise straight line
155      * segments will be drawn. Defaults to false.
156      */
157     smooth: false,
158
159     /**
160      * @cfg {Boolean} fill
161      * If true, the area below the line will be filled in using the {@link #style.eefill} and
162      * {@link #style.opacity} config properties. Defaults to false.
163      */
164     fill: false,
165
166     constructor: function(config) {
167         this.callParent(arguments);
168         var me = this,
169             surface = me.chart.surface,
170             shadow = me.chart.shadow,
171             i, l;
172         Ext.apply(me, config, {
173             highlightCfg: {
174                 'stroke-width': 3
175             },
176             shadowAttributes: [{
177                 "stroke-width": 6,
178                 "stroke-opacity": 0.05,
179                 stroke: 'rgb(0, 0, 0)',
180                 translate: {
181                     x: 1,
182                     y: 1
183                 }
184             }, {
185                 "stroke-width": 4,
186                 "stroke-opacity": 0.1,
187                 stroke: 'rgb(0, 0, 0)',
188                 translate: {
189                     x: 1,
190                     y: 1
191                 }
192             }, {
193                 "stroke-width": 2,
194                 "stroke-opacity": 0.15,
195                 stroke: 'rgb(0, 0, 0)',
196                 translate: {
197                     x: 1,
198                     y: 1
199                 }
200             }]
201         });
202         me.group = surface.getGroup(me.seriesId);
203         if (me.showMarkers) {
204             me.markerGroup = surface.getGroup(me.seriesId + '-markers');
205         }
206         if (shadow) {
207             for (i = 0, l = this.shadowAttributes.length; i < l; i++) {
208                 me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
209             }
210         }
211     },
212     
213     // @private makes an average of points when there are more data points than pixels to be rendered.
214     shrink: function(xValues, yValues, size) {
215         // Start at the 2nd point...
216         var len = xValues.length,
217             ratio = Math.floor(len / size),
218             i = 1,
219             xSum = 0,
220             ySum = 0,
221             xRes = [xValues[0]],
222             yRes = [yValues[0]];
223         
224         for (; i < len; ++i) {
225             xSum += xValues[i] || 0;
226             ySum += yValues[i] || 0;
227             if (i % ratio == 0) {
228                 xRes.push(xSum/ratio);
229                 yRes.push(ySum/ratio);
230                 xSum = 0;
231                 ySum = 0;
232             }
233         }
234         return {
235             x: xRes,
236             y: yRes
237         };
238     },
239
240     /**
241      * Draws the series for the current chart.
242      */
243     drawSeries: function() {
244         var me = this,
245             chart = me.chart,
246             store = chart.substore || chart.store,
247             surface = chart.surface,
248             chartBBox = chart.chartBBox,
249             bbox = {},
250             group = me.group,
251             gutterX = chart.maxGutter[0],
252             gutterY = chart.maxGutter[1],
253             showMarkers = me.showMarkers,
254             markerGroup = me.markerGroup,
255             enableShadows = chart.shadow,
256             shadowGroups = me.shadowGroups,
257             shadowAttributes = this.shadowAttributes,
258             lnsh = shadowGroups.length,
259             dummyPath = ["M"],
260             path = ["M"],
261             markerIndex = chart.markerIndex,
262             axes = [].concat(me.axis),
263             shadowGroup,
264             shadowBarAttr,
265             xValues = [],
266             yValues = [],
267             numericAxis = true,
268             axisCount = 0,
269             onbreak = false,
270             markerStyle = me.markerStyle,
271             seriesStyle = me.seriesStyle,
272             seriesLabelStyle = me.seriesLabelStyle,
273             colorArrayStyle = me.colorArrayStyle,
274             colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
275             posHash = {
276                 'left': 'right',
277                 'right': 'left',
278                 'top': 'bottom',
279                 'bottom': 'top'
280             },
281             seriesIdx = me.seriesIdx, shadows, shadow, shindex, fromPath, fill, fillPath, rendererAttributes,
282             x, y, prevX, prevY, firstY, markerCount, i, j, ln, axis, ends, marker, markerAux, item, xValue,
283             yValue, coords, xScale, yScale, minX, maxX, minY, maxY, line, animation, endMarkerStyle,
284             endLineStyle, type, props, firstMarker, count;
285         
286         //if store is empty then there's nothing to draw.
287         if (!store || !store.getCount()) {
288             return;
289         }
290         
291         //prepare style objects for line and markers
292         endMarkerStyle = Ext.apply(markerStyle, me.markerConfig);
293         type = endMarkerStyle.type;
294         delete endMarkerStyle.type;
295         endLineStyle = Ext.apply(seriesStyle, me.style);
296         //if no stroke with is specified force it to 0.5 because this is
297         //about making *lines*
298         if (!endLineStyle['stroke-width']) {
299             endLineStyle['stroke-width'] = 0.5;
300         }
301         //If we're using a time axis and we need to translate the points,
302         //then reuse the first markers as the last markers.
303         if (markerIndex && markerGroup && markerGroup.getCount()) {
304             for (i = 0; i < markerIndex; i++) {
305                 marker = markerGroup.getAt(i);
306                 markerGroup.remove(marker);
307                 markerGroup.add(marker);
308                 markerAux = markerGroup.getAt(markerGroup.getCount() - 2);
309                 marker.setAttributes({
310                     x: 0,
311                     y: 0,
312                     translate: {
313                         x: markerAux.attr.translation.x,
314                         y: markerAux.attr.translation.y
315                     }
316                 }, true);
317             }
318         }
319         
320         me.unHighlightItem();
321         me.cleanHighlights();
322
323         me.setBBox();
324         bbox = me.bbox;
325
326         me.clipRect = [bbox.x, bbox.y, bbox.width, bbox.height];
327
328         chart.axes.each(function(axis) {
329             //only apply position calculations to axes that affect this series
330             //this means the axis in the position referred by this series and also
331             //the axis in the other coordinate for this series. For example: (left, top|bottom),
332             //or (top, left|right), etc.
333             if (axis.position == me.axis || axis.position != posHash[me.axis]) {
334                 axisCount++;
335                 if (axis.type != 'Numeric') {
336                     numericAxis = false;
337                     return;
338                 }
339                 numericAxis = (numericAxis && axis.type == 'Numeric');
340                 if (axis) {
341                     ends = axis.calcEnds();
342                     if (axis.position == 'top' || axis.position == 'bottom') {
343                         minX = ends.from;
344                         maxX = ends.to;
345                     }
346                     else {
347                         minY = ends.from;
348                         maxY = ends.to;
349                     }
350                 }
351             }
352         });
353         
354         //If there's only one axis specified for a series, then we set the default type of the other
355         //axis to a category axis. So in this case numericAxis, which would be true if both axes affecting
356         //the series are numeric should be false.
357         if (numericAxis && axisCount == 1) {
358             numericAxis = false;
359         }
360         
361         // If a field was specified without a corresponding axis, create one to get bounds
362         //only do this for the axis where real values are bound (that's why we check for
363         //me.axis)
364         if (me.xField && !Ext.isNumber(minX)) {
365             if (me.axis == 'bottom' || me.axis == 'top') {
366                 axis = Ext.create('Ext.chart.axis.Axis', {
367                     chart: chart,
368                     fields: [].concat(me.xField)
369                 }).calcEnds();
370                 minX = axis.from;
371                 maxX = axis.to;
372             } else if (numericAxis) {
373                 axis = Ext.create('Ext.chart.axis.Axis', {
374                     chart: chart,
375                     fields: [].concat(me.xField),
376                     forceMinMax: true
377                 }).calcEnds();
378                 minX = axis.from;
379                 maxX = axis.to;
380             }
381         }
382         
383         if (me.yField && !Ext.isNumber(minY)) {
384             if (me.axis == 'right' || me.axis == 'left') {
385                 axis = Ext.create('Ext.chart.axis.Axis', {
386                     chart: chart,
387                     fields: [].concat(me.yField)
388                 }).calcEnds();
389                 minY = axis.from;
390                 maxY = axis.to;
391             } else if (numericAxis) {
392                 axis = Ext.create('Ext.chart.axis.Axis', {
393                     chart: chart,
394                     fields: [].concat(me.yField),
395                     forceMinMax: true
396                 }).calcEnds();
397                 minY = axis.from;
398                 maxY = axis.to;
399             }
400         }
401         
402         if (isNaN(minX)) {
403             minX = 0;
404             xScale = bbox.width / (store.getCount() - 1);
405         }
406         else {
407             xScale = bbox.width / (maxX - minX);
408         }
409
410         if (isNaN(minY)) {
411             minY = 0;
412             yScale = bbox.height / (store.getCount() - 1);
413         } 
414         else {
415             yScale = bbox.height / (maxY - minY);
416         }
417         
418         store.each(function(record, i) {
419             xValue = record.get(me.xField);
420             yValue = record.get(me.yField);
421             //skip undefined values
422             if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) {
423                 //<debug warn>
424                 if (Ext.isDefined(Ext.global.console)) {
425                     Ext.global.console.warn("[Ext.chart.series.Line]  Skipping a store element with an undefined value at ", record, xValue, yValue);
426                 }
427                 //</debug>
428                 return;
429             }
430             // Ensure a value
431             if (typeof xValue == 'string' || typeof xValue == 'object'
432                 //set as uniform distribution if the axis is a category axis.
433                 || (me.axis != 'top' && me.axis != 'bottom' && !numericAxis)) {
434                 xValue = i;
435             }
436             if (typeof yValue == 'string' || typeof yValue == 'object'
437                 //set as uniform distribution if the axis is a category axis.
438                 || (me.axis != 'left' && me.axis != 'right' && !numericAxis)) {
439                 yValue = i;
440             }
441             xValues.push(xValue);
442             yValues.push(yValue);
443         }, me);
444
445         ln = xValues.length;
446         if (ln > bbox.width) {
447             coords = me.shrink(xValues, yValues, bbox.width);
448             xValues = coords.x;
449             yValues = coords.y;
450         }
451
452         me.items = [];
453
454         count = 0;
455         ln = xValues.length;
456         for (i = 0; i < ln; i++) {
457             xValue = xValues[i];
458             yValue = yValues[i];
459             if (yValue === false) {
460                 if (path.length == 1) {
461                     path = [];
462                 }
463                 onbreak = true;
464                 me.items.push(false);
465                 continue;
466             } else {
467                 x = (bbox.x + (xValue - minX) * xScale).toFixed(2);
468                 y = ((bbox.y + bbox.height) - (yValue - minY) * yScale).toFixed(2);
469                 if (onbreak) {
470                     onbreak = false;
471                     path.push('M');
472                 } 
473                 path = path.concat([x, y]);
474             }
475             if ((typeof firstY == 'undefined') && (typeof y != 'undefined')) {
476                 firstY = y;
477             }
478             // If this is the first line, create a dummypath to animate in from.
479             if (!me.line || chart.resizing) {
480                 dummyPath = dummyPath.concat([x, bbox.y + bbox.height / 2]);
481             }
482
483             // When resizing, reset before animating
484             if (chart.animate && chart.resizing && me.line) {
485                 me.line.setAttributes({
486                     path: dummyPath
487                 }, true);
488                 if (me.fillPath) {
489                     me.fillPath.setAttributes({
490                         path: dummyPath,
491                         opacity: 0.2
492                     }, true);
493                 }
494                 if (me.line.shadows) {
495                     shadows = me.line.shadows;
496                     for (j = 0, lnsh = shadows.length; j < lnsh; j++) {
497                         shadow = shadows[j];
498                         shadow.setAttributes({
499                             path: dummyPath
500                         }, true);
501                     }
502                 }
503             }
504             if (showMarkers) {
505                 marker = markerGroup.getAt(count++);
506                 if (!marker) {
507                     marker = Ext.chart.Shape[type](surface, Ext.apply({
508                         group: [group, markerGroup],
509                         x: 0, y: 0,
510                         translate: {
511                             x: prevX || x, 
512                             y: prevY || (bbox.y + bbox.height / 2)
513                         },
514                         value: '"' + xValue + ', ' + yValue + '"'
515                     }, endMarkerStyle));
516                     marker._to = {
517                         translate: {
518                             x: x,
519                             y: y
520                         }
521                     };
522                 } else {
523                     marker.setAttributes({
524                         value: '"' + xValue + ', ' + yValue + '"',
525                         x: 0, y: 0,
526                         hidden: false
527                     }, true);
528                     marker._to = {
529                         translate: {
530                             x: x, y: y
531                         }
532                     };
533                 }
534             }
535             me.items.push({
536                 series: me,
537                 value: [xValue, yValue],
538                 point: [x, y],
539                 sprite: marker,
540                 storeItem: store.getAt(i)
541             });
542             prevX = x;
543             prevY = y;
544         }
545         
546         if (path.length <= 1) {
547             //nothing to be rendered
548             return;    
549         }
550         
551         if (me.smooth) {
552             path = Ext.draw.Draw.smooth(path, 6);
553         }
554         
555         //Correct path if we're animating timeAxis intervals
556         if (chart.markerIndex && me.previousPath) {
557             fromPath = me.previousPath;
558             fromPath.splice(1, 2);
559         } else {
560             fromPath = path;
561         }
562
563         // Only create a line if one doesn't exist.
564         if (!me.line) {
565             me.line = surface.add(Ext.apply({
566                 type: 'path',
567                 group: group,
568                 path: dummyPath,
569                 stroke: endLineStyle.stroke || endLineStyle.fill
570             }, endLineStyle || {}));
571             //unset fill here (there's always a default fill withing the themes).
572             me.line.setAttributes({
573                 fill: 'none'
574             });
575             if (!endLineStyle.stroke && colorArrayLength) {
576                 me.line.setAttributes({
577                     stroke: colorArrayStyle[seriesIdx % colorArrayLength]
578                 }, true);
579             }
580             if (enableShadows) {
581                 //create shadows
582                 shadows = me.line.shadows = [];                
583                 for (shindex = 0; shindex < lnsh; shindex++) {
584                     shadowBarAttr = shadowAttributes[shindex];
585                     shadowBarAttr = Ext.apply({}, shadowBarAttr, { path: dummyPath });
586                     shadow = chart.surface.add(Ext.apply({}, {
587                         type: 'path',
588                         group: shadowGroups[shindex]
589                     }, shadowBarAttr));
590                     shadows.push(shadow);
591                 }
592             }
593         }
594         if (me.fill) {
595             fillPath = path.concat([
596                 ["L", x, bbox.y + bbox.height],
597                 ["L", bbox.x, bbox.y + bbox.height],
598                 ["L", bbox.x, firstY]
599             ]);
600             if (!me.fillPath) {
601                 me.fillPath = surface.add({
602                     group: group,
603                     type: 'path',
604                     opacity: endLineStyle.opacity || 0.3,
605                     fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill,
606                     path: dummyPath
607                 });
608             }
609         }
610         markerCount = showMarkers && markerGroup.getCount();
611         if (chart.animate) {
612             fill = me.fill;
613             line = me.line;
614             //Add renderer to line. There is not unique record associated with this.
615             rendererAttributes = me.renderer(line, false, { path: path }, i, store);
616             Ext.apply(rendererAttributes, endLineStyle || {}, {
617                 stroke: endLineStyle.stroke || endLineStyle.fill
618             });
619             //fill should not be used here but when drawing the special fill path object
620             delete rendererAttributes.fill;
621             if (chart.markerIndex && me.previousPath) {
622                 me.animation = animation = me.onAnimate(line, {
623                     to: rendererAttributes,
624                     from: {
625                         path: fromPath
626                     }
627                 });
628             } else {
629                 me.animation = animation = me.onAnimate(line, {
630                     to: rendererAttributes
631                 });
632             }
633             //animate shadows
634             if (enableShadows) {
635                 shadows = line.shadows;
636                 for(j = 0; j < lnsh; j++) {
637                     if (chart.markerIndex && me.previousPath) {
638                         me.onAnimate(shadows[j], {
639                             to: { path: path },
640                             from: { path: fromPath }
641                         });
642                     } else {
643                         me.onAnimate(shadows[j], {
644                             to: { path: path }
645                         });
646                     }
647                 }
648             }
649             //animate fill path
650             if (fill) {
651                 me.onAnimate(me.fillPath, {
652                     to: Ext.apply({}, {
653                         path: fillPath,
654                         fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill
655                     }, endLineStyle || {})
656                 });
657             }
658             //animate markers
659             if (showMarkers) {
660                 count = 0;
661                 for(i = 0; i < ln; i++) {
662                     if (me.items[i]) {
663                         item = markerGroup.getAt(count++);
664                         if (item) {
665                             rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store);
666                             me.onAnimate(item, {
667                                 to: Ext.apply(rendererAttributes, endMarkerStyle || {})
668                             });
669                         }
670                     } 
671                 }
672                 for(; count < markerCount; count++) {
673                     item = markerGroup.getAt(count);
674                     item.hide(true);
675                 }
676             }
677         } else {
678             rendererAttributes = me.renderer(me.line, false, { path: path, hidden: false }, i, store);
679             Ext.apply(rendererAttributes, endLineStyle || {}, {
680                 stroke: endLineStyle.stroke || endLineStyle.fill
681             });
682             //fill should not be used here but when drawing the special fill path object
683             delete rendererAttributes.fill;
684             me.line.setAttributes(rendererAttributes, true);
685             //set path for shadows
686             if (enableShadows) {
687                 shadows = me.line.shadows;
688                 for(j = 0; j < lnsh; j++) {
689                     shadows[j].setAttributes({
690                         path: path
691                     }, true);
692                 }
693             }
694             if (me.fill) {
695                 me.fillPath.setAttributes({
696                     path: fillPath
697                 }, true);
698             }
699             if (showMarkers) {
700                 count = 0;
701                 for(i = 0; i < ln; i++) {
702                     if (me.items[i]) {
703                         item = markerGroup.getAt(count++);
704                         if (item) {
705                             rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store);
706                             item.setAttributes(Ext.apply(endMarkerStyle || {}, rendererAttributes || {}), true);
707                         }
708                     } 
709                 }
710                 for(; count < markerCount; count++) {
711                     item = markerGroup.getAt(count);
712                     item.hide(true);
713                 }
714             }
715         }
716
717         if (chart.markerIndex) {
718             path.splice(1, 0, path[1], path[2]);
719             me.previousPath = path;
720         }
721         me.renderLabels();
722         me.renderCallouts();
723     },
724     
725     // @private called when a label is to be created.
726     onCreateLabel: function(storeItem, item, i, display) {
727         var me = this,
728             group = me.labelsGroup,
729             config = me.label,
730             bbox = me.bbox,
731             endLabelStyle = Ext.apply(config, me.seriesLabelStyle);
732
733         return me.chart.surface.add(Ext.apply({
734             'type': 'text',
735             'text-anchor': 'middle',
736             'group': group,
737             'x': item.point[0],
738             'y': bbox.y + bbox.height / 2
739         }, endLabelStyle || {}));
740     },
741     
742     // @private called when a label is to be created.
743     onPlaceLabel: function(label, storeItem, item, i, display, animate) {
744         var me = this,
745             chart = me.chart,
746             resizing = chart.resizing,
747             config = me.label,
748             format = config.renderer,
749             field = config.field,
750             bbox = me.bbox,
751             x = item.point[0],
752             y = item.point[1],
753             radius = item.sprite.attr.radius,
754             bb, width, height;
755         
756         label.setAttributes({
757             text: format(storeItem.get(field)),
758             hidden: true
759         }, true);
760         
761         if (display == 'rotate') {
762             label.setAttributes({
763                 'text-anchor': 'start',
764                 'rotation': {
765                     x: x,
766                     y: y,
767                     degrees: -45
768                 }
769             }, true);
770             //correct label position to fit into the box
771             bb = label.getBBox();
772             width = bb.width;
773             height = bb.height;
774             x = x < bbox.x? bbox.x : x;
775             x = (x + width > bbox.x + bbox.width)? (x - (x + width - bbox.x - bbox.width)) : x;
776             y = (y - height < bbox.y)? bbox.y + height : y;
777         
778         } else if (display == 'under' || display == 'over') {
779             //TODO(nicolas): find out why width/height values in circle bounding boxes are undefined.
780             bb = item.sprite.getBBox();
781             bb.width = bb.width || (radius * 2);
782             bb.height = bb.height || (radius * 2);
783             y = y + (display == 'over'? -bb.height : bb.height);
784             //correct label position to fit into the box
785             bb = label.getBBox();
786             width = bb.width/2;
787             height = bb.height/2;
788             x = x - width < bbox.x? bbox.x + width : x;
789             x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
790             y = y - height < bbox.y? bbox.y + height : y;
791             y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
792         }
793         
794         if (me.chart.animate && !me.chart.resizing) {
795             label.show(true);
796             me.onAnimate(label, {
797                 to: {
798                     x: x,
799                     y: y
800                 }
801             });
802         } else {
803             label.setAttributes({
804                 x: x,
805                 y: y
806             }, true);
807             if (resizing) {
808                 me.animation.on('afteranimate', function() {
809                     label.show(true);
810                 });
811             } else {
812                 label.show(true);
813             }
814         }
815     },
816
817     //@private Overriding highlights.js highlightItem method.
818     highlightItem: function() {
819         var me = this;
820         me.callParent(arguments);
821         if (this.line && !this.highlighted) {
822             if (!('__strokeWidth' in this.line)) {
823                 this.line.__strokeWidth = this.line.attr['stroke-width'] || 0;
824             }
825             if (this.line.__anim) {
826                 this.line.__anim.paused = true;
827             }
828             this.line.__anim = Ext.create('Ext.fx.Anim', {
829                 target: this.line,
830                 to: {
831                     'stroke-width': this.line.__strokeWidth + 3
832                 }
833             });
834             this.highlighted = true;
835         }
836     },
837
838     //@private Overriding highlights.js unHighlightItem method.
839     unHighlightItem: function() {
840         var me = this;
841         me.callParent(arguments);
842         if (this.line && this.highlighted) {
843             this.line.__anim = Ext.create('Ext.fx.Anim', {
844                 target: this.line,
845                 to: {
846                     'stroke-width': this.line.__strokeWidth
847                 }
848             });
849             this.highlighted = false;
850         }
851     },
852
853     //@private called when a callout needs to be placed.
854     onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) {
855         if (!display) {
856             return;
857         }
858         
859         var me = this,
860             chart = me.chart,
861             surface = chart.surface,
862             resizing = chart.resizing,
863             config = me.callouts,
864             items = me.items,
865             prev = i == 0? false : items[i -1].point,
866             next = (i == items.length -1)? false : items[i +1].point,
867             cur = [+item.point[0], +item.point[1]],
868             dir, norm, normal, a, aprev, anext,
869             offsetFromViz = config.offsetFromViz || 30,
870             offsetToSide = config.offsetToSide || 10,
871             offsetBox = config.offsetBox || 3,
872             boxx, boxy, boxw, boxh,
873             p, clipRect = me.clipRect,
874             bbox = {
875                 width: config.styles.width || 10,
876                 height: config.styles.height || 10
877             },
878             x, y;
879
880         //get the right two points
881         if (!prev) {
882             prev = cur;
883         }
884         if (!next) {
885             next = cur;
886         }
887         a = (next[1] - prev[1]) / (next[0] - prev[0]);
888         aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]);
889         anext = (next[1] - cur[1]) / (next[0] - cur[0]);
890         
891         norm = Math.sqrt(1 + a * a);
892         dir = [1 / norm, a / norm];
893         normal = [-dir[1], dir[0]];
894         
895         //keep the label always on the outer part of the "elbow"
896         if (aprev > 0 && anext < 0 && normal[1] < 0
897             || aprev < 0 && anext > 0 && normal[1] > 0) {
898             normal[0] *= -1;
899             normal[1] *= -1;
900         } else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0
901                    || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) {
902             normal[0] *= -1;
903             normal[1] *= -1;
904         }
905         //position
906         x = cur[0] + normal[0] * offsetFromViz;
907         y = cur[1] + normal[1] * offsetFromViz;
908
909         //box position and dimensions
910         boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
911         boxy = y - bbox.height /2 - offsetBox;
912         boxw = bbox.width + 2 * offsetBox;
913         boxh = bbox.height + 2 * offsetBox;
914         
915         //now check if we're out of bounds and invert the normal vector correspondingly
916         //this may add new overlaps between labels (but labels won't be out of bounds).
917         if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) {
918             normal[0] *= -1;
919         }
920         if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) {
921             normal[1] *= -1;
922         }
923
924         //update positions
925         x = cur[0] + normal[0] * offsetFromViz;
926         y = cur[1] + normal[1] * offsetFromViz;
927         
928         //update box position and dimensions
929         boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
930         boxy = y - bbox.height /2 - offsetBox;
931         boxw = bbox.width + 2 * offsetBox;
932         boxh = bbox.height + 2 * offsetBox;
933         
934         if (chart.animate) {
935             //set the line from the middle of the pie to the box.
936             me.onAnimate(callout.lines, {
937                 to: {
938                     path: ["M", cur[0], cur[1], "L", x, y, "Z"]
939                 }
940             });
941             //set component position
942             if (callout.panel) {
943                 callout.panel.setPosition(boxx, boxy, true);
944             }
945         }
946         else {
947             //set the line from the middle of the pie to the box.
948             callout.lines.setAttributes({
949                 path: ["M", cur[0], cur[1], "L", x, y, "Z"]
950             }, true);
951             //set component position
952             if (callout.panel) {
953                 callout.panel.setPosition(boxx, boxy);
954             }
955         }
956         for (p in callout) {
957             callout[p].show(true);
958         }
959     },
960     
961     isItemInPoint: function(x, y, item, i) {
962         var me = this,
963             items = me.items,
964             tolerance = me.selectionTolerance,
965             result = null,
966             prevItem,
967             nextItem,
968             prevPoint,
969             nextPoint,
970             ln,
971             x1,
972             y1,
973             x2,
974             y2,
975             xIntersect,
976             yIntersect,
977             dist1, dist2, dist, midx, midy,
978             sqrt = Math.sqrt, abs = Math.abs;
979         
980         nextItem = items[i];
981         prevItem = i && items[i - 1];
982         
983         if (i >= ln) {
984             prevItem = items[ln - 1];
985         }
986         prevPoint = prevItem && prevItem.point;
987         nextPoint = nextItem && nextItem.point;
988         x1 = prevItem ? prevPoint[0] : nextPoint[0] - tolerance;
989         y1 = prevItem ? prevPoint[1] : nextPoint[1];
990         x2 = nextItem ? nextPoint[0] : prevPoint[0] + tolerance;
991         y2 = nextItem ? nextPoint[1] : prevPoint[1];
992         dist1 = sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1));
993         dist2 = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2));
994         dist = Math.min(dist1, dist2);
995         
996         if (dist <= tolerance) {
997             return dist == dist1? prevItem : nextItem;
998         }
999         return false;
1000     },
1001     
1002     // @private toggle visibility of all series elements (markers, sprites).
1003     toggleAll: function(show) {
1004         var me = this,
1005             i, ln, shadow, shadows;
1006         if (!show) {
1007             Ext.chart.series.Line.superclass.hideAll.call(me);
1008         }
1009         else {
1010             Ext.chart.series.Line.superclass.showAll.call(me);
1011         }
1012         if (me.line) {
1013             me.line.setAttributes({
1014                 hidden: !show
1015             }, true);
1016             //hide shadows too
1017             if (me.line.shadows) {
1018                 for (i = 0, shadows = me.line.shadows, ln = shadows.length; i < ln; i++) {
1019                     shadow = shadows[i];
1020                     shadow.setAttributes({
1021                         hidden: !show
1022                     }, true);
1023                 }
1024             }
1025         }
1026         if (me.fillPath) {
1027             me.fillPath.setAttributes({
1028                 hidden: !show
1029             }, true);
1030         }
1031     },
1032     
1033     // @private hide all series elements (markers, sprites).
1034     hideAll: function() {
1035         this.toggleAll(false);
1036     },
1037     
1038     // @private hide all series elements (markers, sprites).
1039     showAll: function() {
1040         this.toggleAll(true);
1041     }
1042 });