X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/chart/series/Scatter.js?ds=sidebyside
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);
+ }
+});
+