X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/chart/series/Scatter.js diff --git a/src/chart/series/Scatter.js b/src/chart/series/Scatter.js new file mode 100644 index 00000000..12ee2a5f --- /dev/null +++ b/src/chart/series/Scatter.js @@ -0,0 +1,656 @@ +/** + * @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: + * +{@img Ext.chart.series.Scatter/Ext.chart.series.Scatter.png Ext.chart.series.Scatter chart series} +

+    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: 'bottom',
+            fields: ['data1', 'data2', 'data3'],
+            title: 'Sample Values',
+            grid: true,
+            minimum: 0
+        }, {
+            type: 'Category',
+            position: 'left',
+            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. + */ + + 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.substore || chart.store, + 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.substore || chart.store, + 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)) { + // + 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); + } + // + return; + } + // Ensure a value + if (typeof xValue == 'string' || typeof xValue == 'object') { + xValue = i; + } + if (typeof yValue == 'string' || typeof yValue == 'object') { + 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.substore || chart.store, + 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({}, { + 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), 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, 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); + } +}); +