2 * @class Ext.chart.series.Scatter
3 * @extends Ext.chart.series.Cartesian
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:
10 {@img Ext.chart.series.Scatter/Ext.chart.series.Scatter.png Ext.chart.series.Scatter chart series}
12 var store = Ext.create('Ext.data.JsonStore', {
13 fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
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}
23 Ext.create('Ext.chart.Chart', {
24 renderTo: Ext.getBody(),
33 fields: ['data1', 'data2', 'data3'],
34 title: 'Sample Values',
41 title: 'Sample Metrics'
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.
74 Ext.define('Ext.chart.series.Scatter', {
76 /* Begin Definitions */
78 extend: 'Ext.chart.series.Cartesian',
80 requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shape', 'Ext.fx.Anim'],
85 alias: 'series.scatter',
88 * @cfg {Object} markerConfig
89 * The display style for the scatter series markers.
94 * Append styling properties to this object for it to override theme properties.
97 constructor: function(config) {
98 this.callParent(arguments);
100 shadow = me.chart.shadow,
101 surface = me.chart.surface, i, l;
102 Ext.apply(me, config, {
107 "stroke-opacity": 0.05,
108 stroke: 'rgb(0, 0, 0)'
111 "stroke-opacity": 0.1,
112 stroke: 'rgb(0, 0, 0)'
115 "stroke-opacity": 0.15,
116 stroke: 'rgb(0, 0, 0)'
119 me.group = surface.getGroup(me.seriesId);
121 for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
122 me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
127 // @private Get chart and data boundaries
128 getBounds: function() {
131 store = chart.substore || chart.store,
132 axes = [].concat(me.axis),
133 bbox, xScale, yScale, ln, minX, minY, maxX, maxY, i, axis, ends;
138 for (i = 0, ln = axes.length; i < ln; i++) {
139 axis = chart.axes.get(axes[i]);
141 ends = axis.calcEnds();
142 if (axis.position == 'top' || axis.position == 'bottom') {
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', {
156 fields: [].concat(me.xField)
161 if (me.yField && !Ext.isNumber(minY)) {
162 axis = Ext.create('Ext.chart.axis.Axis', {
164 fields: [].concat(me.yField)
172 maxX = store.getCount() - 1;
173 xScale = bbox.width / (store.getCount() - 1);
176 xScale = bbox.width / (maxX - minX);
181 maxY = store.getCount() - 1;
182 yScale = bbox.height / (store.getCount() - 1);
185 yScale = bbox.height / (maxY - minY);
197 // @private Build an array of paths for the chart
198 getPaths: function() {
201 enableShadows = chart.shadow,
202 store = chart.substore || chart.store,
204 bounds = me.bounds = me.getBounds(),
206 xScale = bounds.xScale,
207 yScale = bounds.yScale,
212 boxHeight = bbox.height,
213 items = me.items = [],
215 x, y, xValue, yValue, sprite;
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)) {
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);
230 if (typeof xValue == 'string' || typeof xValue == 'object') {
233 if (typeof yValue == 'string' || typeof yValue == 'object') {
236 x = boxX + (xValue - minX) * xScale;
237 y = boxY + boxHeight - (yValue - minY) * yScale;
245 value: [xValue, yValue],
250 // When resizing, reset before animating
251 if (chart.animate && chart.resizing) {
252 sprite = group.getAt(i);
254 me.resetPoint(sprite);
256 me.resetShadow(sprite);
264 // @private translate point to the center
265 resetPoint: function(sprite) {
266 var bbox = this.bbox;
267 sprite.setAttributes({
269 x: (bbox.x + bbox.width) / 2,
270 y: (bbox.y + bbox.height) / 2
275 // @private translate shadows of a sprite to the center
276 resetShadow: function(sprite) {
278 shadows = sprite.shadows,
279 shadowAttributes = me.shadowAttributes,
280 ln = me.shadowGroups.length,
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;
291 x: (bbox.x + bbox.width) / 2,
292 y: (bbox.y + bbox.height) / 2
295 shadows[i].setAttributes(attr, true);
299 // @private create a new point
300 createPoint: function(attr, type) {
306 return Ext.chart.Shape[type](chart.surface, Ext.apply({}, {
311 x: (bbox.x + bbox.width) / 2,
312 y: (bbox.y + bbox.height) / 2
317 // @private create a new set of shadows for a sprite
318 createShadow: function(sprite, endMarkerStyle, type) {
321 shadowGroups = me.shadowGroups,
322 shadowAttributes = me.shadowAttributes,
323 lnsh = shadowGroups.length,
325 i, shadow, shadows, attr;
327 sprite.shadows = shadows = [];
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;
338 x: (bbox.x + bbox.width) / 2,
339 y: (bbox.y + bbox.height) / 2
343 Ext.apply(attr, endMarkerStyle);
344 shadow = Ext.chart.Shape[type](chart.surface, Ext.apply({}, {
347 group: shadowGroups[i]
349 shadows.push(shadow);
354 * Draws the series for the current chart.
356 drawSeries: function() {
359 store = chart.substore || chart.store,
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;
368 endMarkerStyle = Ext.apply(me.markerStyle, me.markerConfig);
369 type = endMarkerStyle.type;
370 delete endMarkerStyle.type;
372 //if the store is empty then there's nothing to be rendered
373 if (!store || !store.getCount()) {
377 me.unHighlightItem();
378 me.cleanHighlights();
380 attrs = me.getPaths();
382 for (i = 0; i < ln; i++) {
384 sprite = group.getAt(i);
385 Ext.apply(attr, endMarkerStyle);
387 // Create a new sprite if needed (no height)
389 sprite = me.createPoint(attr, type);
391 me.createShadow(sprite, endMarkerStyle, type);
395 shadows = sprite.shadows;
397 rendererAttributes = me.renderer(sprite, store.getAt(i), { translate: attr }, i, store);
398 sprite._to = rendererAttributes;
399 me.onAnimate(sprite, {
400 to: rendererAttributes
403 for (shindex = 0; shindex < lnsh; shindex++) {
404 shadowAttribute = Ext.apply({}, shadowAttributes[shindex]);
405 rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({}, {
407 x: attr.x + (shadowAttribute.translate? shadowAttribute.translate.x : 0),
408 y: attr.y + (shadowAttribute.translate? shadowAttribute.translate.y : 0)
410 }, shadowAttribute), i, store);
411 me.onAnimate(shadows[shindex], { to: rendererAttributes });
415 rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply({ translate: attr }, { hidden: false }), i, store);
416 sprite.setAttributes(rendererAttributes, true);
418 for (shindex = 0; shindex < lnsh; shindex++) {
419 shadowAttribute = shadowAttributes[shindex];
420 rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({
423 }, shadowAttribute), i, store);
424 shadows[shindex].setAttributes(rendererAttributes, true);
427 me.items[i].sprite = sprite;
430 // Hide unused sprites
431 ln = group.getCount();
432 for (i = attrs.length; i < ln; i++) {
433 group.getAt(i).hide(true);
439 // @private callback for when creating a label sprite.
440 onCreateLabel: function(storeItem, item, i, display) {
442 group = me.labelsGroup,
444 endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle),
447 return me.chart.surface.add(Ext.apply({
451 y: bbox.y + bbox.height / 2
455 // @private callback for when placing a label sprite.
456 onPlaceLabel: function(label, storeItem, item, i, display, animate) {
459 resizing = chart.resizing,
461 format = config.renderer,
462 field = config.field,
466 radius = item.sprite.attr.radius,
467 bb, width, height, anim;
469 label.setAttributes({
470 text: format(storeItem.get(field)),
474 if (display == 'rotate') {
475 label.setAttributes({
476 'text-anchor': 'start',
483 //correct label position to fit into the box
484 bb = label.getBBox();
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;
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();
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;
507 if (!chart.animate) {
508 label.setAttributes({
516 anim = item.sprite.getActiveAnimation();
518 anim.on('afteranimate', function() {
519 label.setAttributes({
531 me.onAnimate(label, {
541 // @private callback for when placing a callout sprite.
542 onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) {
545 surface = chart.surface,
546 resizing = chart.resizing,
547 config = me.callouts,
551 bbox = callout.label.getBBox(),
555 boxx, boxy, boxw, boxh,
556 p, clipRect = me.bbox,
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;
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;
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])) {
575 if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) {
580 x = cur[0] + normal[0] * offsetFromViz;
581 y = cur[1] + normal[1] * offsetFromViz;
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;
590 //set the line from the middle of the pie to the box.
591 me.onAnimate(callout.lines, {
593 path: ["M", cur[0], cur[1], "L", x, y, "Z"]
597 me.onAnimate(callout.box, {
606 me.onAnimate(callout.label, {
608 x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)),
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"]
618 callout.box.setAttributes({
625 callout.label.setAttributes({
626 x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)),
631 callout[p].show(true);
635 // @private handles sprite animation for the series.
636 onAnimate: function(sprite, attr) {
638 return this.callParent(arguments);
641 isItemInPoint: function(x, y, item) {
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);
652 return (point[0] - tolerance <= x && point[0] + tolerance >= x &&
653 point[1] - tolerance <= y && point[1] + tolerance >= y);