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