3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.chart.series.Scatter
17 * @extends Ext.chart.series.Cartesian
19 * Creates a Scatter Chart. The scatter plot is useful when trying to display more than two variables in the same visualization.
20 * These variables can be mapped into x, y coordinates and also to an element's radius/size, color, etc.
21 * As with all other series, the Scatter Series must be appended in the *series* Chart array configuration. See the Chart
22 * documentation for more information on creating charts. A typical configuration object for the scatter could be:
25 * var store = Ext.create('Ext.data.JsonStore', {
26 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
28 * { 'name': 'metric one', 'data1': 10, 'data2': 12, 'data3': 14, 'data4': 8, 'data5': 13 },
29 * { 'name': 'metric two', 'data1': 7, 'data2': 8, 'data3': 16, 'data4': 10, 'data5': 3 },
30 * { 'name': 'metric three', 'data1': 5, 'data2': 2, 'data3': 14, 'data4': 12, 'data5': 7 },
31 * { 'name': 'metric four', 'data1': 2, 'data2': 14, 'data3': 6, 'data4': 1, 'data5': 23 },
32 * { 'name': 'metric five', 'data1': 27, 'data2': 38, 'data3': 36, 'data4': 13, 'data5': 33 }
36 * Ext.create('Ext.chart.Chart', {
37 * renderTo: Ext.getBody(),
46 * fields: ['data2', 'data3'],
47 * title: 'Sample Values',
54 * title: 'Sample Metrics'
77 * 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,
78 * `data1`, `data2` and `data3` respectively. All x-fields for the series must be the same field, in this case `name`.
79 * Each scatter series has a different styling configuration for markers, specified by the `markerConfig` object. Finally we set the left axis as
80 * axis to show the current values of the elements.
84 Ext.define('Ext.chart.series.Scatter', {
86 /* Begin Definitions */
88 extend: 'Ext.chart.series.Cartesian',
90 requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shape', 'Ext.fx.Anim'],
95 alias: 'series.scatter',
98 * @cfg {Object} markerConfig
99 * The display style for the scatter series markers.
103 * @cfg {Object} style
104 * Append styling properties to this object for it to override theme properties.
108 * @cfg {String/Array} axis
109 * The position of the axis to bind the values to. Possible values are 'left', 'bottom', 'top' and 'right'.
110 * You must explicitly set this value to bind the values of the line series to the ones in the axis, otherwise a
111 * relative scale will be used. If multiple axes are being used, they should both be specified in in the configuration.
114 constructor: function(config) {
115 this.callParent(arguments);
117 shadow = me.chart.shadow,
118 surface = me.chart.surface, i, l;
119 Ext.apply(me, config, {
124 "stroke-opacity": 0.05,
125 stroke: 'rgb(0, 0, 0)'
128 "stroke-opacity": 0.1,
129 stroke: 'rgb(0, 0, 0)'
132 "stroke-opacity": 0.15,
133 stroke: 'rgb(0, 0, 0)'
136 me.group = surface.getGroup(me.seriesId);
138 for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
139 me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
144 // @private Get chart and data boundaries
145 getBounds: function() {
148 store = chart.getChartStore(),
149 axes = [].concat(me.axis),
150 bbox, xScale, yScale, ln, minX, minY, maxX, maxY, i, axis, ends;
155 for (i = 0, ln = axes.length; i < ln; i++) {
156 axis = chart.axes.get(axes[i]);
158 ends = axis.calcEnds();
159 if (axis.position == 'top' || axis.position == 'bottom') {
169 // If a field was specified without a corresponding axis, create one to get bounds
170 if (me.xField && !Ext.isNumber(minX)) {
171 axis = Ext.create('Ext.chart.axis.Axis', {
173 fields: [].concat(me.xField)
178 if (me.yField && !Ext.isNumber(minY)) {
179 axis = Ext.create('Ext.chart.axis.Axis', {
181 fields: [].concat(me.yField)
189 maxX = store.getCount() - 1;
190 xScale = bbox.width / (store.getCount() - 1);
193 xScale = bbox.width / (maxX - minX);
198 maxY = store.getCount() - 1;
199 yScale = bbox.height / (store.getCount() - 1);
202 yScale = bbox.height / (maxY - minY);
214 // @private Build an array of paths for the chart
215 getPaths: function() {
218 enableShadows = chart.shadow,
219 store = chart.getChartStore(),
221 bounds = me.bounds = me.getBounds(),
223 xScale = bounds.xScale,
224 yScale = bounds.yScale,
229 boxHeight = bbox.height,
230 items = me.items = [],
232 x, y, xValue, yValue, sprite;
234 store.each(function(record, i) {
235 xValue = record.get(me.xField);
236 yValue = record.get(me.yField);
237 //skip undefined values
238 if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) {
240 if (Ext.isDefined(Ext.global.console)) {
241 Ext.global.console.warn("[Ext.chart.series.Scatter] Skipping a store element with an undefined value at ", record, xValue, yValue);
247 if (typeof xValue == 'string' || typeof xValue == 'object' && !Ext.isDate(xValue)) {
250 if (typeof yValue == 'string' || typeof yValue == 'object' && !Ext.isDate(yValue)) {
253 x = boxX + (xValue - minX) * xScale;
254 y = boxY + boxHeight - (yValue - minY) * yScale;
262 value: [xValue, yValue],
267 // When resizing, reset before animating
268 if (chart.animate && chart.resizing) {
269 sprite = group.getAt(i);
271 me.resetPoint(sprite);
273 me.resetShadow(sprite);
281 // @private translate point to the center
282 resetPoint: function(sprite) {
283 var bbox = this.bbox;
284 sprite.setAttributes({
286 x: (bbox.x + bbox.width) / 2,
287 y: (bbox.y + bbox.height) / 2
292 // @private translate shadows of a sprite to the center
293 resetShadow: function(sprite) {
295 shadows = sprite.shadows,
296 shadowAttributes = me.shadowAttributes,
297 ln = me.shadowGroups.length,
300 for (i = 0; i < ln; i++) {
301 attr = Ext.apply({}, shadowAttributes[i]);
302 if (attr.translate) {
303 attr.translate.x += (bbox.x + bbox.width) / 2;
304 attr.translate.y += (bbox.y + bbox.height) / 2;
308 x: (bbox.x + bbox.width) / 2,
309 y: (bbox.y + bbox.height) / 2
312 shadows[i].setAttributes(attr, true);
316 // @private create a new point
317 createPoint: function(attr, type) {
323 return Ext.chart.Shape[type](chart.surface, Ext.apply({}, {
328 x: (bbox.x + bbox.width) / 2,
329 y: (bbox.y + bbox.height) / 2
334 // @private create a new set of shadows for a sprite
335 createShadow: function(sprite, endMarkerStyle, type) {
338 shadowGroups = me.shadowGroups,
339 shadowAttributes = me.shadowAttributes,
340 lnsh = shadowGroups.length,
342 i, shadow, shadows, attr;
344 sprite.shadows = shadows = [];
346 for (i = 0; i < lnsh; i++) {
347 attr = Ext.apply({}, shadowAttributes[i]);
348 if (attr.translate) {
349 attr.translate.x += (bbox.x + bbox.width) / 2;
350 attr.translate.y += (bbox.y + bbox.height) / 2;
355 x: (bbox.x + bbox.width) / 2,
356 y: (bbox.y + bbox.height) / 2
360 Ext.apply(attr, endMarkerStyle);
361 shadow = Ext.chart.Shape[type](chart.surface, Ext.apply({}, {
364 group: shadowGroups[i]
366 shadows.push(shadow);
371 * Draws the series for the current chart.
373 drawSeries: function() {
376 store = chart.getChartStore(),
378 enableShadows = chart.shadow,
379 shadowGroups = me.shadowGroups,
380 shadowAttributes = me.shadowAttributes,
381 lnsh = shadowGroups.length,
382 sprite, attrs, attr, ln, i, endMarkerStyle, shindex, type, shadows,
383 rendererAttributes, shadowAttribute;
385 endMarkerStyle = Ext.apply(me.markerStyle, me.markerConfig);
386 type = endMarkerStyle.type;
387 delete endMarkerStyle.type;
389 //if the store is empty then there's nothing to be rendered
390 if (!store || !store.getCount()) {
394 me.unHighlightItem();
395 me.cleanHighlights();
397 attrs = me.getPaths();
399 for (i = 0; i < ln; i++) {
401 sprite = group.getAt(i);
402 Ext.apply(attr, endMarkerStyle);
404 // Create a new sprite if needed (no height)
406 sprite = me.createPoint(attr, type);
408 me.createShadow(sprite, endMarkerStyle, type);
412 shadows = sprite.shadows;
414 rendererAttributes = me.renderer(sprite, store.getAt(i), { translate: attr }, i, store);
415 sprite._to = rendererAttributes;
416 me.onAnimate(sprite, {
417 to: rendererAttributes
420 for (shindex = 0; shindex < lnsh; shindex++) {
421 shadowAttribute = Ext.apply({}, shadowAttributes[shindex]);
422 rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({}, {
425 x: attr.x + (shadowAttribute.translate? shadowAttribute.translate.x : 0),
426 y: attr.y + (shadowAttribute.translate? shadowAttribute.translate.y : 0)
428 }, shadowAttribute), i, store);
429 me.onAnimate(shadows[shindex], { to: rendererAttributes });
433 rendererAttributes = me.renderer(sprite, store.getAt(i), { translate: attr }, i, store);
434 sprite._to = rendererAttributes;
435 sprite.setAttributes(rendererAttributes, true);
437 for (shindex = 0; shindex < lnsh; shindex++) {
438 shadowAttribute = Ext.apply({}, shadowAttributes[shindex]);
439 rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({}, {
442 x: attr.x + (shadowAttribute.translate? shadowAttribute.translate.x : 0),
443 y: attr.y + (shadowAttribute.translate? shadowAttribute.translate.y : 0)
445 }, shadowAttribute), i, store);
446 shadows[shindex].setAttributes(rendererAttributes, true);
449 me.items[i].sprite = sprite;
452 // Hide unused sprites
453 ln = group.getCount();
454 for (i = attrs.length; i < ln; i++) {
455 group.getAt(i).hide(true);
461 // @private callback for when creating a label sprite.
462 onCreateLabel: function(storeItem, item, i, display) {
464 group = me.labelsGroup,
466 endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle),
469 return me.chart.surface.add(Ext.apply({
473 y: bbox.y + bbox.height / 2
477 // @private callback for when placing a label sprite.
478 onPlaceLabel: function(label, storeItem, item, i, display, animate) {
481 resizing = chart.resizing,
483 format = config.renderer,
484 field = config.field,
488 radius = item.sprite.attr.radius,
489 bb, width, height, anim;
491 label.setAttributes({
492 text: format(storeItem.get(field)),
496 if (display == 'rotate') {
497 label.setAttributes({
498 'text-anchor': 'start',
505 //correct label position to fit into the box
506 bb = label.getBBox();
509 x = x < bbox.x? bbox.x : x;
510 x = (x + width > bbox.x + bbox.width)? (x - (x + width - bbox.x - bbox.width)) : x;
511 y = (y - height < bbox.y)? bbox.y + height : y;
513 } else if (display == 'under' || display == 'over') {
514 //TODO(nicolas): find out why width/height values in circle bounding boxes are undefined.
515 bb = item.sprite.getBBox();
516 bb.width = bb.width || (radius * 2);
517 bb.height = bb.height || (radius * 2);
518 y = y + (display == 'over'? -bb.height : bb.height);
519 //correct label position to fit into the box
520 bb = label.getBBox();
522 height = bb.height/2;
523 x = x - width < bbox.x ? bbox.x + width : x;
524 x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
525 y = y - height < bbox.y? bbox.y + height : y;
526 y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
529 if (!chart.animate) {
530 label.setAttributes({
538 anim = item.sprite.getActiveAnimation();
540 anim.on('afteranimate', function() {
541 label.setAttributes({
553 me.onAnimate(label, {
563 // @private callback for when placing a callout sprite.
564 onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) {
567 surface = chart.surface,
568 resizing = chart.resizing,
569 config = me.callouts,
573 bbox = callout.label.getBBox(),
577 boxx, boxy, boxw, boxh,
578 p, clipRect = me.bbox,
582 normal = [Math.cos(Math.PI /4), -Math.sin(Math.PI /4)];
583 x = cur[0] + normal[0] * offsetFromViz;
584 y = cur[1] + normal[1] * offsetFromViz;
586 //box position and dimensions
587 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
588 boxy = y - bbox.height /2 - offsetBox;
589 boxw = bbox.width + 2 * offsetBox;
590 boxh = bbox.height + 2 * offsetBox;
592 //now check if we're out of bounds and invert the normal vector correspondingly
593 //this may add new overlaps between labels (but labels won't be out of bounds).
594 if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) {
597 if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) {
602 x = cur[0] + normal[0] * offsetFromViz;
603 y = cur[1] + normal[1] * offsetFromViz;
605 //update box position and dimensions
606 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
607 boxy = y - bbox.height /2 - offsetBox;
608 boxw = bbox.width + 2 * offsetBox;
609 boxh = bbox.height + 2 * offsetBox;
612 //set the line from the middle of the pie to the box.
613 me.onAnimate(callout.lines, {
615 path: ["M", cur[0], cur[1], "L", x, y, "Z"]
619 me.onAnimate(callout.box, {
628 me.onAnimate(callout.label, {
630 x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)),
635 //set the line from the middle of the pie to the box.
636 callout.lines.setAttributes({
637 path: ["M", cur[0], cur[1], "L", x, y, "Z"]
640 callout.box.setAttributes({
647 callout.label.setAttributes({
648 x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)),
653 callout[p].show(true);
657 // @private handles sprite animation for the series.
658 onAnimate: function(sprite, attr) {
660 return this.callParent(arguments);
663 isItemInPoint: function(x, y, item) {
668 function dist(point) {
669 var dx = abs(point[0] - x),
670 dy = abs(point[1] - y);
671 return Math.sqrt(dx * dx + dy * dy);
674 return (point[0] - tolerance <= x && point[0] + tolerance >= x &&
675 point[1] - tolerance <= y && point[1] + tolerance >= y);