Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / docs / source / Scatter.html
1 <!DOCTYPE html>
2 <html>
3 <head>
4   <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5   <title>The source code</title>
6   <link href="../prettify/prettify.css" type="text/css" rel="stylesheet" />
7   <script type="text/javascript" src="../prettify/prettify.js"></script>
8   <style type="text/css">
9     .highlight { display: block; background-color: #ddd; }
10   </style>
11   <script type="text/javascript">
12     function highlight() {
13       document.getElementById(location.hash.replace(/#/, "")).className = "highlight";
14     }
15   </script>
16 </head>
17 <body onload="prettyPrint(); highlight();">
18   <pre class="prettyprint lang-js"><span id='Ext-chart-series-Scatter'>/**
19 </span> * @class Ext.chart.series.Scatter
20  * @extends Ext.chart.series.Cartesian
21  * 
22  * Creates a Scatter Chart. The scatter plot is useful when trying to display more than two variables in the same visualization. 
23  * These variables can be mapped into x, y coordinates and also to an element's radius/size, color, etc.
24  * As with all other series, the Scatter Series must be appended in the *series* Chart array configuration. See the Chart 
25  * documentation for more information on creating charts. A typical configuration object for the scatter could be:
26  *
27  * {@img Ext.chart.series.Scatter/Ext.chart.series.Scatter.png Ext.chart.series.Scatter chart series}  
28  *
29  *     var store = Ext.create('Ext.data.JsonStore', {
30  *         fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
31  *         data: [
32  *             {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
33  *             {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
34  *             {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
35  *             {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
36  *             {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}                                                
37  *         ]
38  *     });
39  *     
40  *     Ext.create('Ext.chart.Chart', {
41  *         renderTo: Ext.getBody(),
42  *         width: 500,
43  *         height: 300,
44  *         animate: true,
45  *         theme:'Category2',
46  *         store: store,
47  *         axes: [{
48  *             type: 'Numeric',
49  *             position: 'bottom',
50  *             fields: ['data1', 'data2', 'data3'],
51  *             title: 'Sample Values',
52  *             grid: true,
53  *             minimum: 0
54  *         }, {
55  *             type: 'Category',
56  *             position: 'left',
57  *             fields: ['name'],
58  *             title: 'Sample Metrics'
59  *         }],
60  *         series: [{
61  *             type: 'scatter',
62  *             markerConfig: {
63  *                 radius: 5,
64  *                 size: 5
65  *             },
66  *             axis: 'left',
67  *             xField: 'name',
68  *             yField: 'data2'
69  *         }, {
70  *             type: 'scatter',
71  *             markerConfig: {
72  *                 radius: 5,
73  *                 size: 5
74  *             },
75  *             axis: 'left',
76  *             xField: 'name',
77  *             yField: 'data3'
78  *         }]   
79  *     });
80  * 
81  * In this configuration we add three different categories of scatter series. Each of them is bound to a different field of the same data store, 
82  * `data1`, `data2` and `data3` respectively. All x-fields for the series must be the same field, in this case `name`. 
83  * Each scatter series has a different styling configuration for markers, specified by the `markerConfig` object. Finally we set the left axis as 
84  * axis to show the current values of the elements.
85  * 
86  * @xtype scatter
87  */
88 Ext.define('Ext.chart.series.Scatter', {
89
90     /* Begin Definitions */
91
92     extend: 'Ext.chart.series.Cartesian',
93
94     requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shape', 'Ext.fx.Anim'],
95
96     /* End Definitions */
97
98     type: 'scatter',
99     alias: 'series.scatter',
100
101 <span id='Ext-chart-series-Scatter-cfg-markerConfig'>    /**
102 </span>     * @cfg {Object} markerConfig
103      * The display style for the scatter series markers.
104      */
105     
106 <span id='Ext-chart-series-Scatter-cfg-style'>    /**
107 </span>     * @cfg {Object} style 
108      * Append styling properties to this object for it to override theme properties.
109      */
110
111     constructor: function(config) {
112         this.callParent(arguments);
113         var me = this,
114             shadow = me.chart.shadow,
115             surface = me.chart.surface, i, l;
116         Ext.apply(me, config, {
117             style: {},
118             markerConfig: {},
119             shadowAttributes: [{
120                 &quot;stroke-width&quot;: 6,
121                 &quot;stroke-opacity&quot;: 0.05,
122                 stroke: 'rgb(0, 0, 0)'
123             }, {
124                 &quot;stroke-width&quot;: 4,
125                 &quot;stroke-opacity&quot;: 0.1,
126                 stroke: 'rgb(0, 0, 0)'
127             }, {
128                 &quot;stroke-width&quot;: 2,
129                 &quot;stroke-opacity&quot;: 0.15,
130                 stroke: 'rgb(0, 0, 0)'
131             }]
132         });
133         me.group = surface.getGroup(me.seriesId);
134         if (shadow) {
135             for (i = 0, l = me.shadowAttributes.length; i &lt; l; i++) {
136                 me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
137             }
138         }
139     },
140
141     // @private Get chart and data boundaries
142     getBounds: function() {
143         var me = this,
144             chart = me.chart,
145             store = chart.substore || chart.store,
146             axes = [].concat(me.axis),
147             bbox, xScale, yScale, ln, minX, minY, maxX, maxY, i, axis, ends;
148
149         me.setBBox();
150         bbox = me.bbox;
151
152         for (i = 0, ln = axes.length; i &lt; ln; i++) { 
153             axis = chart.axes.get(axes[i]);
154             if (axis) {
155                 ends = axis.calcEnds();
156                 if (axis.position == 'top' || axis.position == 'bottom') {
157                     minX = ends.from;
158                     maxX = ends.to;
159                 }
160                 else {
161                     minY = ends.from;
162                     maxY = ends.to;
163                 }
164             }
165         }
166         // If a field was specified without a corresponding axis, create one to get bounds
167         if (me.xField &amp;&amp; !Ext.isNumber(minX)) {
168             axis = Ext.create('Ext.chart.axis.Axis', {
169                 chart: chart,
170                 fields: [].concat(me.xField)
171             }).calcEnds();
172             minX = axis.from;
173             maxX = axis.to;
174         }
175         if (me.yField &amp;&amp; !Ext.isNumber(minY)) {
176             axis = Ext.create('Ext.chart.axis.Axis', {
177                 chart: chart,
178                 fields: [].concat(me.yField)
179             }).calcEnds();
180             minY = axis.from;
181             maxY = axis.to;
182         }
183
184         if (isNaN(minX)) {
185             minX = 0;
186             maxX = store.getCount() - 1;
187             xScale = bbox.width / (store.getCount() - 1);
188         }
189         else {
190             xScale = bbox.width / (maxX - minX);
191         }
192
193         if (isNaN(minY)) {
194             minY = 0;
195             maxY = store.getCount() - 1;
196             yScale = bbox.height / (store.getCount() - 1);
197         } 
198         else {
199             yScale = bbox.height / (maxY - minY);
200         }
201
202         return {
203             bbox: bbox,
204             minX: minX,
205             minY: minY,
206             xScale: xScale,
207             yScale: yScale
208         };
209     },
210
211     // @private Build an array of paths for the chart
212     getPaths: function() {
213         var me = this,
214             chart = me.chart,
215             enableShadows = chart.shadow,
216             store = chart.substore || chart.store,
217             group = me.group,
218             bounds = me.bounds = me.getBounds(),
219             bbox = me.bbox,
220             xScale = bounds.xScale,
221             yScale = bounds.yScale,
222             minX = bounds.minX,
223             minY = bounds.minY,
224             boxX = bbox.x,
225             boxY = bbox.y,
226             boxHeight = bbox.height,
227             items = me.items = [],
228             attrs = [],
229             x, y, xValue, yValue, sprite;
230
231         store.each(function(record, i) {
232             xValue = record.get(me.xField);
233             yValue = record.get(me.yField);
234             //skip undefined values
235             if (typeof yValue == 'undefined' || (typeof yValue == 'string' &amp;&amp; !yValue)) {
236                 //&lt;debug warn&gt;
237                 if (Ext.isDefined(Ext.global.console)) {
238                     Ext.global.console.warn(&quot;[Ext.chart.series.Scatter]  Skipping a store element with an undefined value at &quot;, record, xValue, yValue);
239                 }
240                 //&lt;/debug&gt;
241                 return;
242             }
243             // Ensure a value
244             if (typeof xValue == 'string' || typeof xValue == 'object') {
245                 xValue = i;
246             }
247             if (typeof yValue == 'string' || typeof yValue == 'object') {
248                 yValue = i;
249             }
250             x = boxX + (xValue - minX) * xScale;
251             y = boxY + boxHeight - (yValue - minY) * yScale;
252             attrs.push({
253                 x: x,
254                 y: y
255             });
256
257             me.items.push({
258                 series: me,
259                 value: [xValue, yValue],
260                 point: [x, y],
261                 storeItem: record
262             });
263
264             // When resizing, reset before animating
265             if (chart.animate &amp;&amp; chart.resizing) {
266                 sprite = group.getAt(i);
267                 if (sprite) {
268                     me.resetPoint(sprite);
269                     if (enableShadows) {
270                         me.resetShadow(sprite);
271                     }
272                 }
273             }
274         });
275         return attrs;
276     },
277
278     // @private translate point to the center
279     resetPoint: function(sprite) {
280         var bbox = this.bbox;
281         sprite.setAttributes({
282             translate: {
283                 x: (bbox.x + bbox.width) / 2,
284                 y: (bbox.y + bbox.height) / 2
285             }
286         }, true);
287     },
288
289     // @private translate shadows of a sprite to the center
290     resetShadow: function(sprite) {
291         var me = this,
292             shadows = sprite.shadows,
293             shadowAttributes = me.shadowAttributes,
294             ln = me.shadowGroups.length,
295             bbox = me.bbox,
296             i, attr;
297         for (i = 0; i &lt; ln; i++) {
298             attr = Ext.apply({}, shadowAttributes[i]);
299             if (attr.translate) {
300                 attr.translate.x += (bbox.x + bbox.width) / 2;
301                 attr.translate.y += (bbox.y + bbox.height) / 2;
302             }
303             else {
304                 attr.translate = {
305                     x: (bbox.x + bbox.width) / 2,
306                     y: (bbox.y + bbox.height) / 2
307                 };
308             }
309             shadows[i].setAttributes(attr, true);
310         }
311     },
312
313     // @private create a new point
314     createPoint: function(attr, type) {
315         var me = this,
316             chart = me.chart,
317             group = me.group,
318             bbox = me.bbox;
319
320         return Ext.chart.Shape[type](chart.surface, Ext.apply({}, {
321             x: 0,
322             y: 0,
323             group: group,
324             translate: {
325                 x: (bbox.x + bbox.width) / 2,
326                 y: (bbox.y + bbox.height) / 2
327             }
328         }, attr));
329     },
330
331     // @private create a new set of shadows for a sprite
332     createShadow: function(sprite, endMarkerStyle, type) {
333         var me = this,
334             chart = me.chart,
335             shadowGroups = me.shadowGroups,
336             shadowAttributes = me.shadowAttributes,
337             lnsh = shadowGroups.length,
338             bbox = me.bbox,
339             i, shadow, shadows, attr;
340
341         sprite.shadows = shadows = [];
342
343         for (i = 0; i &lt; lnsh; i++) {
344             attr = Ext.apply({}, shadowAttributes[i]);
345             if (attr.translate) {
346                 attr.translate.x += (bbox.x + bbox.width) / 2;
347                 attr.translate.y += (bbox.y + bbox.height) / 2;
348             }
349             else {
350                 Ext.apply(attr, {
351                     translate: {
352                         x: (bbox.x + bbox.width) / 2,
353                         y: (bbox.y + bbox.height) / 2
354                     }
355                 });
356             }
357             Ext.apply(attr, endMarkerStyle);
358             shadow = Ext.chart.Shape[type](chart.surface, Ext.apply({}, {
359                 x: 0,
360                 y: 0,
361                 group: shadowGroups[i]
362             }, attr));
363             shadows.push(shadow);
364         }
365     },
366
367 <span id='Ext-chart-series-Scatter-method-drawSeries'>    /**
368 </span>     * Draws the series for the current chart.
369      */
370     drawSeries: function() {
371         var me = this,
372             chart = me.chart,
373             store = chart.substore || chart.store,
374             group = me.group,
375             enableShadows = chart.shadow,
376             shadowGroups = me.shadowGroups,
377             shadowAttributes = me.shadowAttributes,
378             lnsh = shadowGroups.length,
379             sprite, attrs, attr, ln, i, endMarkerStyle, shindex, type, shadows,
380             rendererAttributes, shadowAttribute;
381
382         endMarkerStyle = Ext.apply(me.markerStyle, me.markerConfig);
383         type = endMarkerStyle.type;
384         delete endMarkerStyle.type;
385
386         //if the store is empty then there's nothing to be rendered
387         if (!store || !store.getCount()) {
388             return;
389         }
390
391         me.unHighlightItem();
392         me.cleanHighlights();
393
394         attrs = me.getPaths();
395         ln = attrs.length;
396         for (i = 0; i &lt; ln; i++) {
397             attr = attrs[i];
398             sprite = group.getAt(i);
399             Ext.apply(attr, endMarkerStyle);
400
401             // Create a new sprite if needed (no height)
402             if (!sprite) {
403                 sprite = me.createPoint(attr, type);
404                 if (enableShadows) {
405                     me.createShadow(sprite, endMarkerStyle, type);
406                 }
407             }
408
409             shadows = sprite.shadows;
410             if (chart.animate) {
411                 rendererAttributes = me.renderer(sprite, store.getAt(i), { translate: attr }, i, store);
412                 sprite._to = rendererAttributes;
413                 me.onAnimate(sprite, {
414                     to: rendererAttributes
415                 });
416                 //animate shadows
417                 for (shindex = 0; shindex &lt; lnsh; shindex++) {
418                     shadowAttribute = Ext.apply({}, shadowAttributes[shindex]);
419                     rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({}, { 
420                         translate: {
421                             x: attr.x + (shadowAttribute.translate? shadowAttribute.translate.x : 0),
422                             y: attr.y + (shadowAttribute.translate? shadowAttribute.translate.y : 0)
423                         } 
424                     }, shadowAttribute), i, store);
425                     me.onAnimate(shadows[shindex], { to: rendererAttributes });
426                 }
427             }
428             else {
429                 rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply({ translate: attr }, { hidden: false }), i, store);
430                 sprite.setAttributes(rendererAttributes, true);
431                 //update shadows
432                 for (shindex = 0; shindex &lt; lnsh; shindex++) {
433                     shadowAttribute = shadowAttributes[shindex];
434                     rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({ 
435                         x: attr.x,
436                         y: attr.y
437                     }, shadowAttribute), i, store);
438                     shadows[shindex].setAttributes(rendererAttributes, true);
439                 }
440             }
441             me.items[i].sprite = sprite;
442         }
443
444         // Hide unused sprites
445         ln = group.getCount();
446         for (i = attrs.length; i &lt; ln; i++) {
447             group.getAt(i).hide(true);
448         }
449         me.renderLabels();
450         me.renderCallouts();
451     },
452     
453     // @private callback for when creating a label sprite.
454     onCreateLabel: function(storeItem, item, i, display) {
455         var me = this,
456             group = me.labelsGroup,
457             config = me.label,
458             endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle),
459             bbox = me.bbox;
460         
461         return me.chart.surface.add(Ext.apply({
462             type: 'text',
463             group: group,
464             x: item.point[0],
465             y: bbox.y + bbox.height / 2
466         }, endLabelStyle));
467     },
468     
469     // @private callback for when placing a label sprite.
470     onPlaceLabel: function(label, storeItem, item, i, display, animate) {
471         var me = this,
472             chart = me.chart,
473             resizing = chart.resizing,
474             config = me.label,
475             format = config.renderer,
476             field = config.field,
477             bbox = me.bbox,
478             x = item.point[0],
479             y = item.point[1],
480             radius = item.sprite.attr.radius,
481             bb, width, height, anim;
482         
483         label.setAttributes({
484             text: format(storeItem.get(field)),
485             hidden: true
486         }, true);
487         
488         if (display == 'rotate') {
489             label.setAttributes({
490                 'text-anchor': 'start',
491                 'rotation': {
492                     x: x,
493                     y: y,
494                     degrees: -45
495                 }
496             }, true);
497             //correct label position to fit into the box
498             bb = label.getBBox();
499             width = bb.width;
500             height = bb.height;
501             x = x &lt; bbox.x? bbox.x : x;
502             x = (x + width &gt; bbox.x + bbox.width)? (x - (x + width - bbox.x - bbox.width)) : x;
503             y = (y - height &lt; bbox.y)? bbox.y + height : y;
504         
505         } else if (display == 'under' || display == 'over') {
506             //TODO(nicolas): find out why width/height values in circle bounding boxes are undefined.
507             bb = item.sprite.getBBox();
508             bb.width = bb.width || (radius * 2);
509             bb.height = bb.height || (radius * 2);
510             y = y + (display == 'over'? -bb.height : bb.height);
511             //correct label position to fit into the box
512             bb = label.getBBox();
513             width = bb.width/2;
514             height = bb.height/2;
515             x = x - width &lt; bbox.x ? bbox.x + width : x;
516             x = (x + width &gt; bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
517             y = y - height &lt; bbox.y? bbox.y + height : y;
518             y = (y + height &gt; bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
519         }
520
521         if (!chart.animate) {
522             label.setAttributes({
523                 x: x,
524                 y: y
525             }, true);
526             label.show(true);
527         }
528         else {
529             if (resizing) {
530                 anim = item.sprite.getActiveAnimation();
531                 if (anim) {
532                     anim.on('afteranimate', function() {
533                         label.setAttributes({
534                             x: x,
535                             y: y
536                         }, true);
537                         label.show(true);
538                     });   
539                 }
540                 else {
541                     label.show(true);
542                 }
543             }
544             else {
545                 me.onAnimate(label, {
546                     to: {
547                         x: x,
548                         y: y
549                     }
550                 });
551             }
552         }
553     },
554     
555     // @private callback for when placing a callout sprite.    
556     onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) {
557         var me = this,
558             chart = me.chart,
559             surface = chart.surface,
560             resizing = chart.resizing,
561             config = me.callouts,
562             items = me.items,
563             cur = item.point,
564             normal,
565             bbox = callout.label.getBBox(),
566             offsetFromViz = 30,
567             offsetToSide = 10,
568             offsetBox = 3,
569             boxx, boxy, boxw, boxh,
570             p, clipRect = me.bbox,
571             x, y;
572     
573         //position
574         normal = [Math.cos(Math.PI /4), -Math.sin(Math.PI /4)];
575         x = cur[0] + normal[0] * offsetFromViz;
576         y = cur[1] + normal[1] * offsetFromViz;
577         
578         //box position and dimensions
579         boxx = x + (normal[0] &gt; 0? 0 : -(bbox.width + 2 * offsetBox));
580         boxy = y - bbox.height /2 - offsetBox;
581         boxw = bbox.width + 2 * offsetBox;
582         boxh = bbox.height + 2 * offsetBox;
583         
584         //now check if we're out of bounds and invert the normal vector correspondingly
585         //this may add new overlaps between labels (but labels won't be out of bounds).
586         if (boxx &lt; clipRect[0] || (boxx + boxw) &gt; (clipRect[0] + clipRect[2])) {
587             normal[0] *= -1;
588         }
589         if (boxy &lt; clipRect[1] || (boxy + boxh) &gt; (clipRect[1] + clipRect[3])) {
590             normal[1] *= -1;
591         }
592     
593         //update positions
594         x = cur[0] + normal[0] * offsetFromViz;
595         y = cur[1] + normal[1] * offsetFromViz;
596         
597         //update box position and dimensions
598         boxx = x + (normal[0] &gt; 0? 0 : -(bbox.width + 2 * offsetBox));
599         boxy = y - bbox.height /2 - offsetBox;
600         boxw = bbox.width + 2 * offsetBox;
601         boxh = bbox.height + 2 * offsetBox;
602         
603         if (chart.animate) {
604             //set the line from the middle of the pie to the box.
605             me.onAnimate(callout.lines, {
606                 to: {
607                     path: [&quot;M&quot;, cur[0], cur[1], &quot;L&quot;, x, y, &quot;Z&quot;]
608                 }
609             }, true);
610             //set box position
611             me.onAnimate(callout.box, {
612                 to: {
613                     x: boxx,
614                     y: boxy,
615                     width: boxw,
616                     height: boxh
617                 }
618             }, true);
619             //set text position
620             me.onAnimate(callout.label, {
621                 to: {
622                     x: x + (normal[0] &gt; 0? offsetBox : -(bbox.width + offsetBox)),
623                     y: y
624                 }
625             }, true);
626         } else {
627             //set the line from the middle of the pie to the box.
628             callout.lines.setAttributes({
629                 path: [&quot;M&quot;, cur[0], cur[1], &quot;L&quot;, x, y, &quot;Z&quot;]
630             }, true);
631             //set box position
632             callout.box.setAttributes({
633                 x: boxx,
634                 y: boxy,
635                 width: boxw,
636                 height: boxh
637             }, true);
638             //set text position
639             callout.label.setAttributes({
640                 x: x + (normal[0] &gt; 0? offsetBox : -(bbox.width + offsetBox)),
641                 y: y
642             }, true);
643         }
644         for (p in callout) {
645             callout[p].show(true);
646         }
647     },
648
649     // @private handles sprite animation for the series.
650     onAnimate: function(sprite, attr) {
651         sprite.show();
652         return this.callParent(arguments);
653     },
654
655     isItemInPoint: function(x, y, item) {
656         var point,
657             tolerance = 10,
658             abs = Math.abs;
659
660         function dist(point) {
661             var dx = abs(point[0] - x),
662                 dy = abs(point[1] - y);
663             return Math.sqrt(dx * dx + dy * dy);
664         }
665         point = item.point;
666         return (point[0] - tolerance &lt;= x &amp;&amp; point[0] + tolerance &gt;= x &amp;&amp;
667             point[1] - tolerance &lt;= y &amp;&amp; point[1] + tolerance &gt;= y);
668     }
669 });
670
671 </pre>
672 </body>
673 </html>