/** * @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: *

        series: [{
            type: 'scatter',
            markerCfg: {
                radius: 5,
                size: 5
            },
            axis: 'left',
            xField: 'name',
            yField: 'data1'
        }, {
            type: 'scatter',
            markerCfg: {
                radius: 5,
                size: 5
            },
            axis: 'left',
            xField: 'name',
            yField: 'data2'
        }, {
            type: 'scatter',
            markerCfg: {
                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 `markerCfg` 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.Shapes', 'Ext.fx.Anim'], /* End Definitions */ type: 'scatter',
/** * @cfg {Object} markerCfg * The display style for the scatter series markers. */
/** * @cfg {Object} style * Append styling properties to this object for it to override theme properties. */ constructor: function(config) { this.callParent(arguments); var me = this, shadow = me.chart.shadow, surface = me.chart.surface, i, l; Ext.apply(me, config, { style: {}, markerCfg: {}, 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)); } } },
/** * Draws the series for the current chart. */ drawSeries: function() { var me = this, chart = me.chart, store = chart.substore || chart.store, surface = chart.surface, chartBBox = chart.chartBBox, group = me.group, animationOptions = null, gutterX = chart.maxGutter[0], gutterY = chart.maxGutter[1], enableShadows = chart.shadow, shadowGroups = me.shadowGroups, shadowAttributes = me.shadowAttributes, lnsh = shadowGroups.length, markerStyle = me.markerStyle, seriesStyle = me.seriesStyle, seriesLabelStyle = me.seriesLabelStyle, colorArrayStyle = me.colorArrayStyle, colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0, seriesIdx = me.seriesIdx, minX = 0, minY = 0, shadowGroup, shadowAttribute, shadows, shadow, baseAttrs, shindex, rendererAttributes, attrs = [], attr, sprite, ln, i, axis, ends, marker, xScale, yScale, maxX, maxY, endMarkerStyle, endSeriesStyle, type; endMarkerStyle = Ext.apply(markerStyle, me.markerCfg); endSeriesStyle = Ext.apply(seriesStyle, me.style); type = endMarkerStyle.type; delete endMarkerStyle.type; me.unHighlightItem(); me.cleanHighlights(); me.bbox = bbox = { x: chartBBox.x + gutterX, y: chartBBox.y + gutterY, width: chartBBox.width - (gutterX * 2), height: chartBBox.height - (gutterY * 2) }; me.clipRect = [bbox.x, bbox.y, bbox.width, bbox.height]; axis = chart.axes.get(me.axis); if (axis) { if (axis.position == 'top' || axis.position == 'bottom') { minX = axis.from; maxX = axis.to; } else { minY = axis.from; maxY = axis.to; } } else { if (me.xField) { ends = new Ext.chart.axis.Axis({ chart: chart, fields: [me.xField] }).calcEnds(); minX = ends.from; maxX = ends.to; } if (me.yField) { ends = new Ext.chart.axis.Axis({ chart: chart, fields: [me.yField] }).calcEnds(); minY = ends.from; maxY = ends.to; } } xScale = bbox.width / (maxX - minX); yScale = bbox.height / (maxY - minY); me.items = []; store.each(function(record, i) { var xValue = record.get(me.xField), yValue = record.get(me.yField), x, y; // Ensure a value if (typeof xValue == 'string') { xValue = i; minX = 0; xScale = bbox.width / (store.getCount() - 1); } if (typeof yValue == 'string') { yValue = i; minY = 0; yScale = bbox.height / (store.getCount() - 1); } x = bbox.x + (xValue - minX) * xScale; y = bbox.y + bbox.height - (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) { sprite.setAttributes({ translate: { x: (bbox.x + bbox.width) / 2, y: (bbox.y + bbox.height) / 2 } }, true); if (enableShadows) { shadows = sprite.shadows; for (shindex = 0; shindex < lnsh; shindex++) { shadowAttribute = Ext.apply({}, shadowAttributes[shindex]); if (shadowAttribute.translate) { shadowAttribute.translate = Ext.apply({}, shadowAttribute.translate || {}); shadowAttribute.translate.x += (bbox.x + bbox.width) / 2; shadowAttribute.translate.y += (bbox.y + bbox.height) / 2; } else { Ext.apply(shadowAttribute, { translate: { x: (bbox.x + bbox.width) / 2, y: (bbox.y + bbox.height) / 2 } }); } shadows[shindex].setAttributes(shadowAttribute, true); } } } } }, me); // Create new or reuse bar sprites and animate/display 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 = Ext.chart.Shapes[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)); if (enableShadows) { sprite.shadows = shadows = []; for (shindex = 0; shindex < lnsh; shindex++) { shadowAttribute = Ext.apply({}, shadowAttributes[shindex]); if (shadowAttribute.translate) { shadowAttribute.translate = Ext.apply({}, shadowAttribute.translate); shadowAttribute.translate.x += (bbox.x + bbox.width) / 2; shadowAttribute.translate.y += (bbox.y + bbox.height) / 2; } else { Ext.apply(shadowAttribute, { translate: { x: (bbox.x + bbox.width) / 2, y: (bbox.y + bbox.height) / 2 } }); } Ext.apply(shadowAttribute, endMarkerStyle); shadow = Ext.chart.Shapes[type](chart.surface, Ext.apply({}, { x: 0, y: 0, group: shadowGroups[shindex] }, shadowAttribute)); shadows.push(shadow); } } } shadows = sprite.shadows; if (chart.animate) { rendererAttributes = me.renderer(sprite, store.getAt(i), { translate: attr }, i, store); sprite._to = rendererAttributes; me.animation = 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({}, { translate: { x: attr.x + (shadowAttribute.translate? shadowAttribute.translate.x : 0), y: attr.y + (shadowAttribute.translate? shadowAttribute.translate.y : 0) } }, shadowAttribute), i, store); this.onAnimate(shadows[shindex], { to: rendererAttributes }); } } else { rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply({ translate: attr }, { hidden: false }), i, store); sprite.setAttributes(rendererAttributes, true); //update shadows for (shindex = 0; shindex < lnsh; shindex++) { shadowAttribute = shadowAttributes[shindex]; rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({ x: attr.x, y: attr.y }, 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; 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 (me.chart.animate && !me.chart.resizing) { label.show(true); me.onAnimate(label, { to: { x: x, y: y } }); } else { label.setAttributes({ x: x, y: y }, true); if (resizing) { if (me.animation) { me.animation.on('afteranimate', function() { label.show(true); }); } else { label.show(true); } } else { label.show(true); } } }, // @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.clipRect, 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); },
/** * For a given x/y point relative to the Surface, find a corresponding item from this * series, if any. * * @param x {Number} The left pointer coordinate. * @param y {Number} The top pointer coordinate. * @return {Object} An object with the item found or null instead. */ getItemForPoint: function(x, y) { var items = this.items, point, closestItem = null, tolerance = 10, i = items && items.length; function dist(point) { var abs = Math.abs, dx = abs(point[0] - x), dy = abs(point[1] - y); return Math.sqrt(dx * dx + dy * dy); } while (i--) { point = items[i].point; if (point[0] - tolerance <= x && point[0] + tolerance >= x && point[1] - tolerance <= y && point[1] + tolerance >= y) { if (!closestItem || (dist(point) < dist(closestItem.point))) { closestItem = items[i]; } } // If we already found a match but no longer match, assume we're moving further // away and exit the loop else if (closestItem) { break; } } return closestItem; } });