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