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