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