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