/** * @class Ext.chart.series.Scatter * @extends Ext.chart.series.Cartesian * * Creates a Scatter Chart. The scatter plot is useful when trying to display more than two variables in the same visualization. * These variables can be mapped into x, y coordinates and also to an element's radius/size, color, etc. * As with all other series, the Scatter Series must be appended in the *series* Chart array configuration. See the Chart * documentation for more information on creating charts. A typical configuration object for the scatter could be: * * @example * var store = Ext.create('Ext.data.JsonStore', { * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'], * data: [ * { 'name': 'metric one', 'data1': 10, 'data2': 12, 'data3': 14, 'data4': 8, 'data5': 13 }, * { 'name': 'metric two', 'data1': 7, 'data2': 8, 'data3': 16, 'data4': 10, 'data5': 3 }, * { 'name': 'metric three', 'data1': 5, 'data2': 2, 'data3': 14, 'data4': 12, 'data5': 7 }, * { 'name': 'metric four', 'data1': 2, 'data2': 14, 'data3': 6, 'data4': 1, 'data5': 23 }, * { 'name': 'metric five', 'data1': 27, 'data2': 38, 'data3': 36, 'data4': 13, 'data5': 33 } * ] * }); * * Ext.create('Ext.chart.Chart', { * renderTo: Ext.getBody(), * width: 500, * height: 300, * animate: true, * theme:'Category2', * store: store, * axes: [{ * type: 'Numeric', * position: 'left', * fields: ['data2', 'data3'], * title: 'Sample Values', * grid: true, * minimum: 0 * }, { * type: 'Category', * position: 'bottom', * fields: ['name'], * title: 'Sample Metrics' * }], * series: [{ * type: 'scatter', * markerConfig: { * radius: 5, * size: 5 * }, * axis: 'left', * xField: 'name', * yField: 'data2' * }, { * type: 'scatter', * markerConfig: { * radius: 5, * size: 5 * }, * axis: 'left', * xField: 'name', * yField: 'data3' * }] * }); * * 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, * `data1`, `data2` and `data3` respectively. All x-fields for the series must be the same field, in this case `name`. * Each scatter series has a different styling configuration for markers, specified by the `markerConfig` object. Finally we set the left axis as * axis to show the current values of the elements. * * @xtype scatter */ Ext.define('Ext.chart.series.Scatter', { /* Begin Definitions */ extend: 'Ext.chart.series.Cartesian', requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shape', 'Ext.fx.Anim'], /* End Definitions */ type: 'scatter', alias: 'series.scatter', /** * @cfg {Object} markerConfig * The display style for the scatter series markers. */ /** * @cfg {Object} style * Append styling properties to this object for it to override theme properties. */ /** * @cfg {String/Array} axis * The position of the axis to bind the values to. Possible values are 'left', 'bottom', 'top' and 'right'. * You must explicitly set this value to bind the values of the line series to the ones in the axis, otherwise a * relative scale will be used. If multiple axes are being used, they should both be specified in in the configuration. */ constructor: function(config) { this.callParent(arguments); var me = this, shadow = me.chart.shadow, surface = me.chart.surface, i, l; Ext.apply(me, config, { style: {}, markerConfig: {}, shadowAttributes: [{ "stroke-width": 6, "stroke-opacity": 0.05, stroke: 'rgb(0, 0, 0)' }, { "stroke-width": 4, "stroke-opacity": 0.1, stroke: 'rgb(0, 0, 0)' }, { "stroke-width": 2, "stroke-opacity": 0.15, stroke: 'rgb(0, 0, 0)' }] }); me.group = surface.getGroup(me.seriesId); if (shadow) { for (i = 0, l = me.shadowAttributes.length; i < l; i++) { me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i)); } } }, // @private Get chart and data boundaries getBounds: function() { var me = this, chart = me.chart, store = chart.getChartStore(), axes = [].concat(me.axis), bbox, xScale, yScale, ln, minX, minY, maxX, maxY, i, axis, ends; me.setBBox(); bbox = me.bbox; for (i = 0, ln = axes.length; i < ln; i++) { axis = chart.axes.get(axes[i]); if (axis) { ends = axis.calcEnds(); if (axis.position == 'top' || axis.position == 'bottom') { minX = ends.from; maxX = ends.to; } else { minY = ends.from; maxY = ends.to; } } } // If a field was specified without a corresponding axis, create one to get bounds if (me.xField && !Ext.isNumber(minX)) { axis = Ext.create('Ext.chart.axis.Axis', { chart: chart, fields: [].concat(me.xField) }).calcEnds(); minX = axis.from; maxX = axis.to; } if (me.yField && !Ext.isNumber(minY)) { axis = Ext.create('Ext.chart.axis.Axis', { chart: chart, fields: [].concat(me.yField) }).calcEnds(); minY = axis.from; maxY = axis.to; } if (isNaN(minX)) { minX = 0; maxX = store.getCount() - 1; xScale = bbox.width / (store.getCount() - 1); } else { xScale = bbox.width / (maxX - minX); } if (isNaN(minY)) { minY = 0; maxY = store.getCount() - 1; yScale = bbox.height / (store.getCount() - 1); } else { yScale = bbox.height / (maxY - minY); } return { bbox: bbox, minX: minX, minY: minY, xScale: xScale, yScale: yScale }; }, // @private Build an array of paths for the chart getPaths: function() { var me = this, chart = me.chart, enableShadows = chart.shadow, store = chart.getChartStore(), group = me.group, bounds = me.bounds = me.getBounds(), bbox = me.bbox, xScale = bounds.xScale, yScale = bounds.yScale, minX = bounds.minX, minY = bounds.minY, boxX = bbox.x, boxY = bbox.y, boxHeight = bbox.height, items = me.items = [], attrs = [], x, y, xValue, yValue, sprite; store.each(function(record, i) { xValue = record.get(me.xField); yValue = record.get(me.yField); //skip undefined values if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) { //<debug warn> if (Ext.isDefined(Ext.global.console)) { Ext.global.console.warn("[Ext.chart.series.Scatter] Skipping a store element with an undefined value at ", record, xValue, yValue); } //</debug> return; } // Ensure a value if (typeof xValue == 'string' || typeof xValue == 'object' && !Ext.isDate(xValue)) { xValue = i; } if (typeof yValue == 'string' || typeof yValue == 'object' && !Ext.isDate(yValue)) { yValue = i; } x = boxX + (xValue - minX) * xScale; y = boxY + boxHeight - (yValue - minY) * yScale; attrs.push({ x: x, y: y }); me.items.push({ series: me, value: [xValue, yValue], point: [x, y], storeItem: record }); // When resizing, reset before animating if (chart.animate && chart.resizing) { sprite = group.getAt(i); if (sprite) { me.resetPoint(sprite); if (enableShadows) { me.resetShadow(sprite); } } } }); return attrs; }, // @private translate point to the center resetPoint: function(sprite) { var bbox = this.bbox; sprite.setAttributes({ translate: { x: (bbox.x + bbox.width) / 2, y: (bbox.y + bbox.height) / 2 } }, true); }, // @private translate shadows of a sprite to the center resetShadow: function(sprite) { var me = this, shadows = sprite.shadows, shadowAttributes = me.shadowAttributes, ln = me.shadowGroups.length, bbox = me.bbox, i, attr; for (i = 0; i < ln; i++) { attr = Ext.apply({}, shadowAttributes[i]); if (attr.translate) { attr.translate.x += (bbox.x + bbox.width) / 2; attr.translate.y += (bbox.y + bbox.height) / 2; } else { attr.translate = { x: (bbox.x + bbox.width) / 2, y: (bbox.y + bbox.height) / 2 }; } shadows[i].setAttributes(attr, true); } }, // @private create a new point createPoint: function(attr, type) { var me = this, chart = me.chart, group = me.group, bbox = me.bbox; return Ext.chart.Shape[type](chart.surface, Ext.apply({}, { x: 0, y: 0, group: group, translate: { x: (bbox.x + bbox.width) / 2, y: (bbox.y + bbox.height) / 2 } }, attr)); }, // @private create a new set of shadows for a sprite createShadow: function(sprite, endMarkerStyle, type) { var me = this, chart = me.chart, shadowGroups = me.shadowGroups, shadowAttributes = me.shadowAttributes, lnsh = shadowGroups.length, bbox = me.bbox, i, shadow, shadows, attr; sprite.shadows = shadows = []; for (i = 0; i < lnsh; i++) { attr = Ext.apply({}, shadowAttributes[i]); if (attr.translate) { attr.translate.x += (bbox.x + bbox.width) / 2; attr.translate.y += (bbox.y + bbox.height) / 2; } else { Ext.apply(attr, { translate: { x: (bbox.x + bbox.width) / 2, y: (bbox.y + bbox.height) / 2 } }); } Ext.apply(attr, endMarkerStyle); shadow = Ext.chart.Shape[type](chart.surface, Ext.apply({}, { x: 0, y: 0, group: shadowGroups[i] }, attr)); shadows.push(shadow); } }, /** * Draws the series for the current chart. */ drawSeries: function() { var me = this, chart = me.chart, store = chart.getChartStore(), group = me.group, enableShadows = chart.shadow, shadowGroups = me.shadowGroups, shadowAttributes = me.shadowAttributes, lnsh = shadowGroups.length, sprite, attrs, attr, ln, i, endMarkerStyle, shindex, type, shadows, rendererAttributes, shadowAttribute; endMarkerStyle = Ext.apply(me.markerStyle, me.markerConfig); type = endMarkerStyle.type; delete endMarkerStyle.type; //if the store is empty then there's nothing to be rendered if (!store || !store.getCount()) { return; } me.unHighlightItem(); me.cleanHighlights(); attrs = me.getPaths(); ln = attrs.length; for (i = 0; i < ln; i++) { attr = attrs[i]; sprite = group.getAt(i); Ext.apply(attr, endMarkerStyle); // Create a new sprite if needed (no height) if (!sprite) { sprite = me.createPoint(attr, type); if (enableShadows) { me.createShadow(sprite, endMarkerStyle, type); } } shadows = sprite.shadows; if (chart.animate) { rendererAttributes = me.renderer(sprite, store.getAt(i), { translate: attr }, i, store); sprite._to = rendererAttributes; me.onAnimate(sprite, { to: rendererAttributes }); //animate shadows for (shindex = 0; shindex < lnsh; shindex++) { shadowAttribute = Ext.apply({}, shadowAttributes[shindex]); rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({}, { hidden: false, translate: { x: attr.x + (shadowAttribute.translate? shadowAttribute.translate.x : 0), y: attr.y + (shadowAttribute.translate? shadowAttribute.translate.y : 0) } }, shadowAttribute), i, store); me.onAnimate(shadows[shindex], { to: rendererAttributes }); } } else { rendererAttributes = me.renderer(sprite, store.getAt(i), { translate: attr }, i, store); sprite._to = rendererAttributes; sprite.setAttributes(rendererAttributes, true); //animate shadows for (shindex = 0; shindex < lnsh; shindex++) { shadowAttribute = Ext.apply({}, shadowAttributes[shindex]); rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({}, { hidden: false, translate: { x: attr.x + (shadowAttribute.translate? shadowAttribute.translate.x : 0), y: attr.y + (shadowAttribute.translate? shadowAttribute.translate.y : 0) } }, shadowAttribute), i, store); shadows[shindex].setAttributes(rendererAttributes, true); } } me.items[i].sprite = sprite; } // Hide unused sprites ln = group.getCount(); for (i = attrs.length; i < ln; i++) { group.getAt(i).hide(true); } me.renderLabels(); me.renderCallouts(); }, // @private callback for when creating a label sprite. onCreateLabel: function(storeItem, item, i, display) { var me = this, group = me.labelsGroup, config = me.label, endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle), bbox = me.bbox; return me.chart.surface.add(Ext.apply({ type: 'text', group: group, x: item.point[0], y: bbox.y + bbox.height / 2 }, endLabelStyle)); }, // @private callback for when placing a label sprite. onPlaceLabel: function(label, storeItem, item, i, display, animate) { var me = this, chart = me.chart, resizing = chart.resizing, config = me.label, format = config.renderer, field = config.field, bbox = me.bbox, x = item.point[0], y = item.point[1], radius = item.sprite.attr.radius, bb, width, height, anim; label.setAttributes({ text: format(storeItem.get(field)), hidden: true }, true); if (display == 'rotate') { label.setAttributes({ 'text-anchor': 'start', 'rotation': { x: x, y: y, degrees: -45 } }, true); //correct label position to fit into the box bb = label.getBBox(); width = bb.width; height = bb.height; x = x < bbox.x? bbox.x : x; x = (x + width > bbox.x + bbox.width)? (x - (x + width - bbox.x - bbox.width)) : x; y = (y - height < bbox.y)? bbox.y + height : y; } else if (display == 'under' || display == 'over') { //TODO(nicolas): find out why width/height values in circle bounding boxes are undefined. bb = item.sprite.getBBox(); bb.width = bb.width || (radius * 2); bb.height = bb.height || (radius * 2); y = y + (display == 'over'? -bb.height : bb.height); //correct label position to fit into the box bb = label.getBBox(); width = bb.width/2; height = bb.height/2; x = x - width < bbox.x ? bbox.x + width : x; x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x; y = y - height < bbox.y? bbox.y + height : y; y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y; } if (!chart.animate) { label.setAttributes({ x: x, y: y }, true); label.show(true); } else { if (resizing) { anim = item.sprite.getActiveAnimation(); if (anim) { anim.on('afteranimate', function() { label.setAttributes({ x: x, y: y }, true); label.show(true); }); } else { label.show(true); } } else { me.onAnimate(label, { to: { x: x, y: y } }); } } }, // @private callback for when placing a callout sprite. onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) { var me = this, chart = me.chart, surface = chart.surface, resizing = chart.resizing, config = me.callouts, items = me.items, cur = item.point, normal, bbox = callout.label.getBBox(), offsetFromViz = 30, offsetToSide = 10, offsetBox = 3, boxx, boxy, boxw, boxh, p, clipRect = me.bbox, x, y; //position normal = [Math.cos(Math.PI /4), -Math.sin(Math.PI /4)]; x = cur[0] + normal[0] * offsetFromViz; y = cur[1] + normal[1] * offsetFromViz; //box position and dimensions boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox)); boxy = y - bbox.height /2 - offsetBox; boxw = bbox.width + 2 * offsetBox; boxh = bbox.height + 2 * offsetBox; //now check if we're out of bounds and invert the normal vector correspondingly //this may add new overlaps between labels (but labels won't be out of bounds). if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) { normal[0] *= -1; } if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) { normal[1] *= -1; } //update positions x = cur[0] + normal[0] * offsetFromViz; y = cur[1] + normal[1] * offsetFromViz; //update box position and dimensions boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox)); boxy = y - bbox.height /2 - offsetBox; boxw = bbox.width + 2 * offsetBox; boxh = bbox.height + 2 * offsetBox; if (chart.animate) { //set the line from the middle of the pie to the box. me.onAnimate(callout.lines, { to: { path: ["M", cur[0], cur[1], "L", x, y, "Z"] } }, true); //set box position me.onAnimate(callout.box, { to: { x: boxx, y: boxy, width: boxw, height: boxh } }, true); //set text position me.onAnimate(callout.label, { to: { x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)), y: y } }, true); } else { //set the line from the middle of the pie to the box. callout.lines.setAttributes({ path: ["M", cur[0], cur[1], "L", x, y, "Z"] }, true); //set box position callout.box.setAttributes({ x: boxx, y: boxy, width: boxw, height: boxh }, true); //set text position callout.label.setAttributes({ x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)), y: y }, true); } for (p in callout) { callout[p].show(true); } }, // @private handles sprite animation for the series. onAnimate: function(sprite, attr) { sprite.show(); return this.callParent(arguments); }, isItemInPoint: function(x, y, item) { var point, tolerance = 10, abs = Math.abs; function dist(point) { var dx = abs(point[0] - x), dy = abs(point[1] - y); return Math.sqrt(dx * dx + dy * dy); } point = item.point; return (point[0] - tolerance <= x && point[0] + tolerance >= x && point[1] - tolerance <= y && point[1] + tolerance >= y); } });