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:
24 * {@img Ext.chart.series.Scatter/Ext.chart.series.Scatter.png Ext.chart.series.Scatter chart series}
26 * var store = Ext.create('Ext.data.JsonStore', {
27 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
29 * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
30 * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
31 * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
32 * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
33 * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
37 * Ext.create('Ext.chart.Chart', {
38 * renderTo: Ext.getBody(),
47 * fields: ['data1', 'data2', 'data3'],
48 * title: 'Sample Values',
55 * title: 'Sample Metrics'
78 * 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,
79 * `data1`, `data2` and `data3` respectively. All x-fields for the series must be the same field, in this case `name`.
80 * Each scatter series has a different styling configuration for markers, specified by the `markerConfig` object. Finally we set the left axis as
81 * axis to show the current values of the elements.
85 Ext.define('Ext.chart.series.Scatter', {
87 /* Begin Definitions */
89 extend: 'Ext.chart.series.Cartesian',
91 requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shape', 'Ext.fx.Anim'],
96 alias: 'series.scatter',
99 * @cfg {Object} markerConfig
100 * The display style for the scatter series markers.
104 * @cfg {Object} style
105 * Append styling properties to this object for it to override theme properties.
108 constructor: function(config) {
109 this.callParent(arguments);
111 shadow = me.chart.shadow,
112 surface = me.chart.surface, i, l;
113 Ext.apply(me, config, {
118 "stroke-opacity": 0.05,
119 stroke: 'rgb(0, 0, 0)'
122 "stroke-opacity": 0.1,
123 stroke: 'rgb(0, 0, 0)'
126 "stroke-opacity": 0.15,
127 stroke: 'rgb(0, 0, 0)'
130 me.group = surface.getGroup(me.seriesId);
132 for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
133 me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
138 // @private Get chart and data boundaries
139 getBounds: function() {
142 store = chart.substore || chart.store,
143 axes = [].concat(me.axis),
144 bbox, xScale, yScale, ln, minX, minY, maxX, maxY, i, axis, ends;
149 for (i = 0, ln = axes.length; i < ln; i++) {
150 axis = chart.axes.get(axes[i]);
152 ends = axis.calcEnds();
153 if (axis.position == 'top' || axis.position == 'bottom') {
163 // If a field was specified without a corresponding axis, create one to get bounds
164 if (me.xField && !Ext.isNumber(minX)) {
165 axis = Ext.create('Ext.chart.axis.Axis', {
167 fields: [].concat(me.xField)
172 if (me.yField && !Ext.isNumber(minY)) {
173 axis = Ext.create('Ext.chart.axis.Axis', {
175 fields: [].concat(me.yField)
183 maxX = store.getCount() - 1;
184 xScale = bbox.width / (store.getCount() - 1);
187 xScale = bbox.width / (maxX - minX);
192 maxY = store.getCount() - 1;
193 yScale = bbox.height / (store.getCount() - 1);
196 yScale = bbox.height / (maxY - minY);
208 // @private Build an array of paths for the chart
209 getPaths: function() {
212 enableShadows = chart.shadow,
213 store = chart.substore || chart.store,
215 bounds = me.bounds = me.getBounds(),
217 xScale = bounds.xScale,
218 yScale = bounds.yScale,
223 boxHeight = bbox.height,
224 items = me.items = [],
226 x, y, xValue, yValue, sprite;
228 store.each(function(record, i) {
229 xValue = record.get(me.xField);
230 yValue = record.get(me.yField);
231 //skip undefined values
232 if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) {
234 if (Ext.isDefined(Ext.global.console)) {
235 Ext.global.console.warn("[Ext.chart.series.Scatter] Skipping a store element with an undefined value at ", record, xValue, yValue);
241 if (typeof xValue == 'string' || typeof xValue == 'object') {
244 if (typeof yValue == 'string' || typeof yValue == 'object') {
247 x = boxX + (xValue - minX) * xScale;
248 y = boxY + boxHeight - (yValue - minY) * yScale;
256 value: [xValue, yValue],
261 // When resizing, reset before animating
262 if (chart.animate && chart.resizing) {
263 sprite = group.getAt(i);
265 me.resetPoint(sprite);
267 me.resetShadow(sprite);
275 // @private translate point to the center
276 resetPoint: function(sprite) {
277 var bbox = this.bbox;
278 sprite.setAttributes({
280 x: (bbox.x + bbox.width) / 2,
281 y: (bbox.y + bbox.height) / 2
286 // @private translate shadows of a sprite to the center
287 resetShadow: function(sprite) {
289 shadows = sprite.shadows,
290 shadowAttributes = me.shadowAttributes,
291 ln = me.shadowGroups.length,
294 for (i = 0; i < ln; i++) {
295 attr = Ext.apply({}, shadowAttributes[i]);
296 if (attr.translate) {
297 attr.translate.x += (bbox.x + bbox.width) / 2;
298 attr.translate.y += (bbox.y + bbox.height) / 2;
302 x: (bbox.x + bbox.width) / 2,
303 y: (bbox.y + bbox.height) / 2
306 shadows[i].setAttributes(attr, true);
310 // @private create a new point
311 createPoint: function(attr, type) {
317 return Ext.chart.Shape[type](chart.surface, Ext.apply({}, {
322 x: (bbox.x + bbox.width) / 2,
323 y: (bbox.y + bbox.height) / 2
328 // @private create a new set of shadows for a sprite
329 createShadow: function(sprite, endMarkerStyle, type) {
332 shadowGroups = me.shadowGroups,
333 shadowAttributes = me.shadowAttributes,
334 lnsh = shadowGroups.length,
336 i, shadow, shadows, attr;
338 sprite.shadows = shadows = [];
340 for (i = 0; i < lnsh; i++) {
341 attr = Ext.apply({}, shadowAttributes[i]);
342 if (attr.translate) {
343 attr.translate.x += (bbox.x + bbox.width) / 2;
344 attr.translate.y += (bbox.y + bbox.height) / 2;
349 x: (bbox.x + bbox.width) / 2,
350 y: (bbox.y + bbox.height) / 2
354 Ext.apply(attr, endMarkerStyle);
355 shadow = Ext.chart.Shape[type](chart.surface, Ext.apply({}, {
358 group: shadowGroups[i]
360 shadows.push(shadow);
365 * Draws the series for the current chart.
367 drawSeries: function() {
370 store = chart.substore || chart.store,
372 enableShadows = chart.shadow,
373 shadowGroups = me.shadowGroups,
374 shadowAttributes = me.shadowAttributes,
375 lnsh = shadowGroups.length,
376 sprite, attrs, attr, ln, i, endMarkerStyle, shindex, type, shadows,
377 rendererAttributes, shadowAttribute;
379 endMarkerStyle = Ext.apply(me.markerStyle, me.markerConfig);
380 type = endMarkerStyle.type;
381 delete endMarkerStyle.type;
383 //if the store is empty then there's nothing to be rendered
384 if (!store || !store.getCount()) {
388 me.unHighlightItem();
389 me.cleanHighlights();
391 attrs = me.getPaths();
393 for (i = 0; i < ln; i++) {
395 sprite = group.getAt(i);
396 Ext.apply(attr, endMarkerStyle);
398 // Create a new sprite if needed (no height)
400 sprite = me.createPoint(attr, type);
402 me.createShadow(sprite, endMarkerStyle, type);
406 shadows = sprite.shadows;
408 rendererAttributes = me.renderer(sprite, store.getAt(i), { translate: attr }, i, store);
409 sprite._to = rendererAttributes;
410 me.onAnimate(sprite, {
411 to: rendererAttributes
414 for (shindex = 0; shindex < lnsh; shindex++) {
415 shadowAttribute = Ext.apply({}, shadowAttributes[shindex]);
416 rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({}, {
418 x: attr.x + (shadowAttribute.translate? shadowAttribute.translate.x : 0),
419 y: attr.y + (shadowAttribute.translate? shadowAttribute.translate.y : 0)
421 }, shadowAttribute), i, store);
422 me.onAnimate(shadows[shindex], { to: rendererAttributes });
426 rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply({ translate: attr }, { hidden: false }), i, store);
427 sprite.setAttributes(rendererAttributes, true);
429 for (shindex = 0; shindex < lnsh; shindex++) {
430 shadowAttribute = shadowAttributes[shindex];
431 rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({
434 }, shadowAttribute), i, store);
435 shadows[shindex].setAttributes(rendererAttributes, true);
438 me.items[i].sprite = sprite;
441 // Hide unused sprites
442 ln = group.getCount();
443 for (i = attrs.length; i < ln; i++) {
444 group.getAt(i).hide(true);
450 // @private callback for when creating a label sprite.
451 onCreateLabel: function(storeItem, item, i, display) {
453 group = me.labelsGroup,
455 endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle),
458 return me.chart.surface.add(Ext.apply({
462 y: bbox.y + bbox.height / 2
466 // @private callback for when placing a label sprite.
467 onPlaceLabel: function(label, storeItem, item, i, display, animate) {
470 resizing = chart.resizing,
472 format = config.renderer,
473 field = config.field,
477 radius = item.sprite.attr.radius,
478 bb, width, height, anim;
480 label.setAttributes({
481 text: format(storeItem.get(field)),
485 if (display == 'rotate') {
486 label.setAttributes({
487 'text-anchor': 'start',
494 //correct label position to fit into the box
495 bb = label.getBBox();
498 x = x < bbox.x? bbox.x : x;
499 x = (x + width > bbox.x + bbox.width)? (x - (x + width - bbox.x - bbox.width)) : x;
500 y = (y - height < bbox.y)? bbox.y + height : y;
502 } else if (display == 'under' || display == 'over') {
503 //TODO(nicolas): find out why width/height values in circle bounding boxes are undefined.
504 bb = item.sprite.getBBox();
505 bb.width = bb.width || (radius * 2);
506 bb.height = bb.height || (radius * 2);
507 y = y + (display == 'over'? -bb.height : bb.height);
508 //correct label position to fit into the box
509 bb = label.getBBox();
511 height = bb.height/2;
512 x = x - width < bbox.x ? bbox.x + width : x;
513 x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
514 y = y - height < bbox.y? bbox.y + height : y;
515 y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
518 if (!chart.animate) {
519 label.setAttributes({
527 anim = item.sprite.getActiveAnimation();
529 anim.on('afteranimate', function() {
530 label.setAttributes({
542 me.onAnimate(label, {
552 // @private callback for when placing a callout sprite.
553 onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) {
556 surface = chart.surface,
557 resizing = chart.resizing,
558 config = me.callouts,
562 bbox = callout.label.getBBox(),
566 boxx, boxy, boxw, boxh,
567 p, clipRect = me.bbox,
571 normal = [Math.cos(Math.PI /4), -Math.sin(Math.PI /4)];
572 x = cur[0] + normal[0] * offsetFromViz;
573 y = cur[1] + normal[1] * offsetFromViz;
575 //box position and dimensions
576 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
577 boxy = y - bbox.height /2 - offsetBox;
578 boxw = bbox.width + 2 * offsetBox;
579 boxh = bbox.height + 2 * offsetBox;
581 //now check if we're out of bounds and invert the normal vector correspondingly
582 //this may add new overlaps between labels (but labels won't be out of bounds).
583 if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) {
586 if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) {
591 x = cur[0] + normal[0] * offsetFromViz;
592 y = cur[1] + normal[1] * offsetFromViz;
594 //update box position and dimensions
595 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
596 boxy = y - bbox.height /2 - offsetBox;
597 boxw = bbox.width + 2 * offsetBox;
598 boxh = bbox.height + 2 * offsetBox;
601 //set the line from the middle of the pie to the box.
602 me.onAnimate(callout.lines, {
604 path: ["M", cur[0], cur[1], "L", x, y, "Z"]
608 me.onAnimate(callout.box, {
617 me.onAnimate(callout.label, {
619 x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)),
624 //set the line from the middle of the pie to the box.
625 callout.lines.setAttributes({
626 path: ["M", cur[0], cur[1], "L", x, y, "Z"]
629 callout.box.setAttributes({
636 callout.label.setAttributes({
637 x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)),
642 callout[p].show(true);
646 // @private handles sprite animation for the series.
647 onAnimate: function(sprite, attr) {
649 return this.callParent(arguments);
652 isItemInPoint: function(x, y, item) {
657 function dist(point) {
658 var dx = abs(point[0] - x),
659 dy = abs(point[1] - y);
660 return Math.sqrt(dx * dx + dy * dy);
663 return (point[0] - tolerance <= x && point[0] + tolerance >= x &&
664 point[1] - tolerance <= y && point[1] + tolerance >= y);