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