X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/chart/series/Line.js diff --git a/src/chart/series/Line.js b/src/chart/series/Line.js new file mode 100644 index 00000000..5369cdd3 --- /dev/null +++ b/src/chart/series/Line.js @@ -0,0 +1,994 @@ +/** + * @class Ext.chart.series.Line + * @extends Ext.chart.series.Cartesian + * +

+ Creates a Line Chart. A Line Chart is a useful visualization technique to display quantitative information for different + categories or other real values (as opposed to the bar chart), that can show some progression (or regression) in the dataset. + As with all other series, the Line Series must be appended in the *series* Chart array configuration. See the Chart + documentation for more information. A typical configuration object for the line series could be: +

+{@img Ext.chart.series.Line/Ext.chart.series.Line.png Ext.chart.series.Line 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,
+        store: store,
+        axes: [{
+            type: 'Numeric',
+            position: 'bottom',
+            fields: ['data1'],
+            label: {
+                renderer: Ext.util.Format.numberRenderer('0,0')
+            },
+            title: 'Sample Values',
+            grid: true,
+            minimum: 0
+        }, {
+            type: 'Category',
+            position: 'left',
+            fields: ['name'],
+            title: 'Sample Metrics'
+        }],
+        series: [{
+            type: 'line',
+            highlight: {
+                size: 7,
+                radius: 7
+            },
+            axis: 'left',
+            xField: 'name',
+            yField: 'data1',
+            markerCfg: {
+                type: 'cross',
+                size: 4,
+                radius: 4,
+                'stroke-width': 0
+            }
+        }, {
+            type: 'line',
+            highlight: {
+                size: 7,
+                radius: 7
+            },
+            axis: 'left',
+            fill: true,
+            xField: 'name',
+            yField: 'data3',
+            markerCfg: {
+                type: 'circle',
+                size: 4,
+                radius: 4,
+                'stroke-width': 0
+            }
+        }]
+    });
+   
+ +

+ In this configuration we're adding two series (or lines), one bound to the `data1` property of the store and the other to `data3`. The type for both configurations is + `line`. The `xField` for both series is the same, the name propert of the store. Both line series share the same axis, the left axis. You can set particular marker + configuration by adding properties onto the markerConfig object. Both series have an object as highlight so that markers animate smoothly to the properties in highlight + when hovered. The second series has `fill=true` which means that the line will also have an area below it of the same color. +

+ */ + +Ext.define('Ext.chart.series.Line', { + + /* Begin Definitions */ + + extend: 'Ext.chart.series.Cartesian', + + alternateClassName: ['Ext.chart.LineSeries', 'Ext.chart.LineChart'], + + requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shape', 'Ext.draw.Draw', 'Ext.fx.Anim'], + + /* End Definitions */ + + type: 'line', + + alias: 'series.line', + + /** + * @cfg {Number} selectionTolerance + * The offset distance from the cursor position to the line series to trigger events (then used for highlighting series, etc). + */ + selectionTolerance: 20, + + /** + * @cfg {Boolean} showMarkers + * Whether markers should be displayed at the data points along the line. If true, + * then the {@link #markerConfig} config item will determine the markers' styling. + */ + showMarkers: true, + + /** + * @cfg {Object} markerConfig + * The display style for the markers. Only used if {@link #showMarkers} is true. + * The markerConfig is a configuration object containing the same set of properties defined in + * the Sprite class. For example, if we were to set red circles as markers to the line series we could + * pass the object: + * +

+        markerConfig: {
+            type: 'circle',
+            radius: 4,
+            'fill': '#f00'
+        }
+     
+ + */ + markerConfig: {}, + + /** + * @cfg {Object} style + * An object containing styles for the visualization lines. These styles will override the theme styles. + * Some options contained within the style object will are described next. + */ + style: {}, + + /** + * @cfg {Boolean} smooth + * If true, the line will be smoothed/rounded around its points, otherwise straight line + * segments will be drawn. Defaults to false. + */ + smooth: false, + + /** + * @cfg {Boolean} fill + * If true, the area below the line will be filled in using the {@link #style.eefill} and + * {@link #style.opacity} config properties. Defaults to false. + */ + fill: false, + + constructor: function(config) { + this.callParent(arguments); + var me = this, + surface = me.chart.surface, + shadow = me.chart.shadow, + i, l; + Ext.apply(me, config, { + highlightCfg: { + 'stroke-width': 3 + }, + shadowAttributes: [{ + "stroke-width": 6, + "stroke-opacity": 0.05, + stroke: 'rgb(0, 0, 0)', + translate: { + x: 1, + y: 1 + } + }, { + "stroke-width": 4, + "stroke-opacity": 0.1, + stroke: 'rgb(0, 0, 0)', + translate: { + x: 1, + y: 1 + } + }, { + "stroke-width": 2, + "stroke-opacity": 0.15, + stroke: 'rgb(0, 0, 0)', + translate: { + x: 1, + y: 1 + } + }] + }); + me.group = surface.getGroup(me.seriesId); + if (me.showMarkers) { + me.markerGroup = surface.getGroup(me.seriesId + '-markers'); + } + if (shadow) { + for (i = 0, l = this.shadowAttributes.length; i < l; i++) { + me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i)); + } + } + }, + + // @private makes an average of points when there are more data points than pixels to be rendered. + shrink: function(xValues, yValues, size) { + // Start at the 2nd point... + var len = xValues.length, + ratio = Math.floor(len / size), + i = 1, + xSum = 0, + ySum = 0, + xRes = [xValues[0]], + yRes = [yValues[0]]; + + for (; i < len; ++i) { + xSum += xValues[i] || 0; + ySum += yValues[i] || 0; + if (i % ratio == 0) { + xRes.push(xSum/ratio); + yRes.push(ySum/ratio); + xSum = 0; + ySum = 0; + } + } + return { + x: xRes, + y: yRes + }; + }, + + /** + * 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, + bbox = {}, + group = me.group, + gutterX = chart.maxGutter[0], + gutterY = chart.maxGutter[1], + showMarkers = me.showMarkers, + markerGroup = me.markerGroup, + enableShadows = chart.shadow, + shadowGroups = me.shadowGroups, + shadowAttributes = this.shadowAttributes, + lnsh = shadowGroups.length, + dummyPath = ["M"], + path = ["M"], + markerIndex = chart.markerIndex, + axes = [].concat(me.axis), + shadowGroup, + shadowBarAttr, + xValues = [], + yValues = [], + onbreak = false, + markerStyle = me.markerStyle, + seriesStyle = me.seriesStyle, + seriesLabelStyle = me.seriesLabelStyle, + colorArrayStyle = me.colorArrayStyle, + colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0, + seriesIdx = me.seriesIdx, shadows, shadow, shindex, fromPath, fill, fillPath, rendererAttributes, + x, y, prevX, prevY, firstY, markerCount, i, j, ln, axis, ends, marker, markerAux, item, xValue, + yValue, coords, xScale, yScale, minX, maxX, minY, maxY, line, animation, endMarkerStyle, + endLineStyle, type, props, firstMarker; + + //if store is empty then there's nothing to draw. + if (!store || !store.getCount()) { + return; + } + + //prepare style objects for line and markers + endMarkerStyle = Ext.apply(markerStyle, me.markerConfig); + type = endMarkerStyle.type; + delete endMarkerStyle.type; + endLineStyle = Ext.apply(seriesStyle, me.style); + //if no stroke with is specified force it to 0.5 because this is + //about making *lines* + if (!endLineStyle['stroke-width']) { + endLineStyle['stroke-width'] = 0.5; + } + //If we're using a time axis and we need to translate the points, + //then reuse the first markers as the last markers. + if (markerIndex && markerGroup && markerGroup.getCount()) { + for (i = 0; i < markerIndex; i++) { + marker = markerGroup.getAt(i); + markerGroup.remove(marker); + markerGroup.add(marker); + markerAux = markerGroup.getAt(markerGroup.getCount() - 2); + marker.setAttributes({ + x: 0, + y: 0, + translate: { + x: markerAux.attr.translation.x, + y: markerAux.attr.translation.y + } + }, true); + } + } + + me.unHighlightItem(); + me.cleanHighlights(); + + me.setBBox(); + bbox = me.bbox; + + me.clipRect = [bbox.x, bbox.y, bbox.width, bbox.height]; + + 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 + //only do this for the axis where real values are bound (that's why we check for + //me.axis) + if (me.xField && !Ext.isNumber(minX) + && (me.axis == 'bottom' || me.axis == 'top')) { + 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) + && (me.axis == 'right' || me.axis == 'left')) { + 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; + xScale = bbox.width / (store.getCount() - 1); + } + else { + xScale = bbox.width / (maxX - minX); + } + + if (isNaN(minY)) { + minY = 0; + yScale = bbox.height / (store.getCount() - 1); + } + else { + yScale = bbox.height / (maxY - minY); + } + + 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.Line] Skipping a store element with an undefined value at ", record, xValue, yValue); + } + // + return; + } + // Ensure a value + if (typeof xValue == 'string' || typeof xValue == 'object' + //set as uniform distribution if the axis is a category axis. + || (me.axis != 'top' && me.axis != 'bottom')) { + xValue = i; + } + if (typeof yValue == 'string' || typeof yValue == 'object' + //set as uniform distribution if the axis is a category axis. + || (me.axis != 'left' && me.axis != 'right')) { + yValue = i; + } + xValues.push(xValue); + yValues.push(yValue); + }, me); + + ln = xValues.length; + if (ln > bbox.width) { + coords = me.shrink(xValues, yValues, bbox.width); + xValues = coords.x; + yValues = coords.y; + } + + me.items = []; + + ln = xValues.length; + for (i = 0; i < ln; i++) { + xValue = xValues[i]; + yValue = yValues[i]; + if (yValue === false) { + if (path.length == 1) { + path = []; + } + onbreak = true; + me.items.push(false); + continue; + } else { + x = (bbox.x + (xValue - minX) * xScale).toFixed(2); + y = ((bbox.y + bbox.height) - (yValue - minY) * yScale).toFixed(2); + if (onbreak) { + onbreak = false; + path.push('M'); + } + path = path.concat([x, y]); + } + if ((typeof firstY == 'undefined') && (typeof y != 'undefined')) { + firstY = y; + } + // If this is the first line, create a dummypath to animate in from. + if (!me.line || chart.resizing) { + dummyPath = dummyPath.concat([x, bbox.y + bbox.height / 2]); + } + + // When resizing, reset before animating + if (chart.animate && chart.resizing && me.line) { + me.line.setAttributes({ + path: dummyPath + }, true); + if (me.fillPath) { + me.fillPath.setAttributes({ + path: dummyPath, + opacity: 0.2 + }, true); + } + if (me.line.shadows) { + shadows = me.line.shadows; + for (j = 0, lnsh = shadows.length; j < lnsh; j++) { + shadow = shadows[j]; + shadow.setAttributes({ + path: dummyPath + }, true); + } + } + } + if (showMarkers) { + marker = markerGroup.getAt(i); + if (!marker) { + marker = Ext.chart.Shape[type](surface, Ext.apply({ + group: [group, markerGroup], + x: 0, y: 0, + translate: { + x: prevX || x, + y: prevY || (bbox.y + bbox.height / 2) + }, + value: '"' + xValue + ', ' + yValue + '"' + }, endMarkerStyle)); + marker._to = { + translate: { + x: x, + y: y + } + }; + } else { + marker.setAttributes({ + value: '"' + xValue + ', ' + yValue + '"', + x: 0, y: 0, + hidden: false + }, true); + marker._to = { + translate: { + x: x, y: y + } + }; + } + } + me.items.push({ + series: me, + value: [xValue, yValue], + point: [x, y], + sprite: marker, + storeItem: store.getAt(i) + }); + prevX = x; + prevY = y; + } + + if (path.length <= 1) { + //nothing to be rendered + return; + } + + if (me.smooth) { + path = Ext.draw.Draw.smooth(path, 6); + } + + //Correct path if we're animating timeAxis intervals + if (chart.markerIndex && me.previousPath) { + fromPath = me.previousPath; + fromPath.splice(1, 2); + } else { + fromPath = path; + } + + // Only create a line if one doesn't exist. + if (!me.line) { + me.line = surface.add(Ext.apply({ + type: 'path', + group: group, + path: dummyPath, + stroke: endLineStyle.stroke || endLineStyle.fill + }, endLineStyle || {})); + //unset fill here (there's always a default fill withing the themes). + me.line.setAttributes({ + fill: 'none' + }); + if (!endLineStyle.stroke && colorArrayLength) { + me.line.setAttributes({ + stroke: colorArrayStyle[seriesIdx % colorArrayLength] + }, true); + } + if (enableShadows) { + //create shadows + shadows = me.line.shadows = []; + for (shindex = 0; shindex < lnsh; shindex++) { + shadowBarAttr = shadowAttributes[shindex]; + shadowBarAttr = Ext.apply({}, shadowBarAttr, { path: dummyPath }); + shadow = chart.surface.add(Ext.apply({}, { + type: 'path', + group: shadowGroups[shindex] + }, shadowBarAttr)); + shadows.push(shadow); + } + } + } + if (me.fill) { + fillPath = path.concat([ + ["L", x, bbox.y + bbox.height], + ["L", bbox.x, bbox.y + bbox.height], + ["L", bbox.x, firstY] + ]); + if (!me.fillPath) { + me.fillPath = surface.add({ + group: group, + type: 'path', + opacity: endLineStyle.opacity || 0.3, + fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill, + path: dummyPath + }); + } + } + markerCount = showMarkers && markerGroup.getCount(); + if (chart.animate) { + fill = me.fill; + line = me.line; + //Add renderer to line. There is not unique record associated with this. + rendererAttributes = me.renderer(line, false, { path: path }, i, store); + Ext.apply(rendererAttributes, endLineStyle || {}, { + stroke: endLineStyle.stroke || endLineStyle.fill + }); + //fill should not be used here but when drawing the special fill path object + delete rendererAttributes.fill; + if (chart.markerIndex && me.previousPath) { + me.animation = animation = me.onAnimate(line, { + to: rendererAttributes, + from: { + path: fromPath + } + }); + } else { + me.animation = animation = me.onAnimate(line, { + to: rendererAttributes + }); + } + //animate shadows + if (enableShadows) { + shadows = line.shadows; + for(j = 0; j < lnsh; j++) { + if (chart.markerIndex && me.previousPath) { + me.onAnimate(shadows[j], { + to: { path: path }, + from: { path: fromPath } + }); + } else { + me.onAnimate(shadows[j], { + to: { path: path } + }); + } + } + } + //animate fill path + if (fill) { + me.onAnimate(me.fillPath, { + to: Ext.apply({}, { + path: fillPath, + fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill + }, endLineStyle || {}) + }); + } + //animate markers + if (showMarkers) { + for(i = 0; i < ln; i++) { + item = markerGroup.getAt(i); + if (item) { + if (me.items[i]) { + rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store); + me.onAnimate(item, { + to: Ext.apply(rendererAttributes, endMarkerStyle || {}) + }); + } else { + item.setAttributes(Ext.apply({ + hidden: true + }, item._to), true); + } + } + } + for(; i < markerCount; i++) { + item = markerGroup.getAt(i); + item.hide(true); + } +// for(i = 0; i < (chart.markerIndex || 0)-1; i++) { +// item = markerGroup.getAt(i); +// item.hide(true); +// } + } + } else { + rendererAttributes = me.renderer(me.line, false, { path: path, hidden: false }, i, store); + Ext.apply(rendererAttributes, endLineStyle || {}, { + stroke: endLineStyle.stroke || endLineStyle.fill + }); + //fill should not be used here but when drawing the special fill path object + delete rendererAttributes.fill; + me.line.setAttributes(rendererAttributes, true); + //set path for shadows + if (enableShadows) { + shadows = me.line.shadows; + for(j = 0; j < lnsh; j++) { + shadows[j].setAttributes({ + path: path + }, true); + } + } + if (me.fill) { + me.fillPath.setAttributes({ + path: fillPath + }, true); + } + if (showMarkers) { + for(i = 0; i < ln; i++) { + item = markerGroup.getAt(i); + if (item) { + if (me.items[i]) { + rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store); + item.setAttributes(Ext.apply(endMarkerStyle || {}, rendererAttributes || {}), true); + } else { + item.hide(true); + } + } + } + for(; i < markerCount; i++) { + item = markerGroup.getAt(i); + item.hide(true); + } + } + } + + if (chart.markerIndex) { + path.splice(1, 0, path[1], path[2]); + me.previousPath = path; + } + me.renderLabels(); + me.renderCallouts(); + }, + + // @private called when a label is to be created. + onCreateLabel: function(storeItem, item, i, display) { + var me = this, + group = me.labelsGroup, + config = me.label, + bbox = me.bbox, + endLabelStyle = Ext.apply(config, me.seriesLabelStyle); + + return me.chart.surface.add(Ext.apply({ + 'type': 'text', + 'text-anchor': 'middle', + 'group': group, + 'x': item.point[0], + 'y': bbox.y + bbox.height / 2 + }, endLabelStyle || {})); + }, + + // @private called when a label is to be created. + 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) { + me.animation.on('afteranimate', function() { + label.show(true); + }); + } else { + label.show(true); + } + } + }, + + //@private Overriding highlights.js highlightItem method. + highlightItem: function() { + var me = this; + me.callParent(arguments); + if (this.line && !this.highlighted) { + if (!('__strokeWidth' in this.line)) { + this.line.__strokeWidth = this.line.attr['stroke-width'] || 0; + } + if (this.line.__anim) { + this.line.__anim.paused = true; + } + this.line.__anim = Ext.create('Ext.fx.Anim', { + target: this.line, + to: { + 'stroke-width': this.line.__strokeWidth + 3 + } + }); + this.highlighted = true; + } + }, + + //@private Overriding highlights.js unHighlightItem method. + unHighlightItem: function() { + var me = this; + me.callParent(arguments); + if (this.line && this.highlighted) { + this.line.__anim = Ext.create('Ext.fx.Anim', { + target: this.line, + to: { + 'stroke-width': this.line.__strokeWidth + } + }); + this.highlighted = false; + } + }, + + //@private called when a callout needs to be placed. + onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) { + if (!display) { + return; + } + + var me = this, + chart = me.chart, + surface = chart.surface, + resizing = chart.resizing, + config = me.callouts, + items = me.items, + prev = i == 0? false : items[i -1].point, + next = (i == items.length -1)? false : items[i +1].point, + cur = [+item.point[0], +item.point[1]], + dir, norm, normal, a, aprev, anext, + offsetFromViz = config.offsetFromViz || 30, + offsetToSide = config.offsetToSide || 10, + offsetBox = config.offsetBox || 3, + boxx, boxy, boxw, boxh, + p, clipRect = me.clipRect, + bbox = { + width: config.styles.width || 10, + height: config.styles.height || 10 + }, + x, y; + + //get the right two points + if (!prev) { + prev = cur; + } + if (!next) { + next = cur; + } + a = (next[1] - prev[1]) / (next[0] - prev[0]); + aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]); + anext = (next[1] - cur[1]) / (next[0] - cur[0]); + + norm = Math.sqrt(1 + a * a); + dir = [1 / norm, a / norm]; + normal = [-dir[1], dir[0]]; + + //keep the label always on the outer part of the "elbow" + if (aprev > 0 && anext < 0 && normal[1] < 0 + || aprev < 0 && anext > 0 && normal[1] > 0) { + normal[0] *= -1; + normal[1] *= -1; + } else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0 + || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) { + normal[0] *= -1; + normal[1] *= -1; + } + //position + 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"] + } + }); + //set component position + if (callout.panel) { + callout.panel.setPosition(boxx, boxy, 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 component position + if (callout.panel) { + callout.panel.setPosition(boxx, boxy); + } + } + for (p in callout) { + callout[p].show(true); + } + }, + + isItemInPoint: function(x, y, item, i) { + var me = this, + items = me.items, + tolerance = me.selectionTolerance, + result = null, + prevItem, + nextItem, + prevPoint, + nextPoint, + ln, + x1, + y1, + x2, + y2, + xIntersect, + yIntersect, + dist1, dist2, dist, midx, midy, + sqrt = Math.sqrt, abs = Math.abs; + + nextItem = items[i]; + prevItem = i && items[i - 1]; + + if (i >= ln) { + prevItem = items[ln - 1]; + } + prevPoint = prevItem && prevItem.point; + nextPoint = nextItem && nextItem.point; + x1 = prevItem ? prevPoint[0] : nextPoint[0] - tolerance; + y1 = prevItem ? prevPoint[1] : nextPoint[1]; + x2 = nextItem ? nextPoint[0] : prevPoint[0] + tolerance; + y2 = nextItem ? nextPoint[1] : prevPoint[1]; + dist1 = sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1)); + dist2 = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2)); + dist = Math.min(dist1, dist2); + + if (dist <= tolerance) { + return dist == dist1? prevItem : nextItem; + } + return false; + }, + + // @private toggle visibility of all series elements (markers, sprites). + toggleAll: function(show) { + var me = this, + i, ln, shadow, shadows; + if (!show) { + Ext.chart.series.Line.superclass.hideAll.call(me); + } + else { + Ext.chart.series.Line.superclass.showAll.call(me); + } + if (me.line) { + me.line.setAttributes({ + hidden: !show + }, true); + //hide shadows too + if (me.line.shadows) { + for (i = 0, shadows = me.line.shadows, ln = shadows.length; i < ln; i++) { + shadow = shadows[i]; + shadow.setAttributes({ + hidden: !show + }, true); + } + } + } + if (me.fillPath) { + me.fillPath.setAttributes({ + hidden: !show + }, true); + } + }, + + // @private hide all series elements (markers, sprites). + hideAll: function() { + this.toggleAll(false); + }, + + // @private hide all series elements (markers, sprites). + showAll: function() { + this.toggleAll(true); + } +}); \ No newline at end of file