--- /dev/null
+<!DOCTYPE html><html><head><title>Sencha Documentation Project</title><link rel="stylesheet" href="../reset.css" type="text/css"><link rel="stylesheet" href="../prettify.css" type="text/css"><link rel="stylesheet" href="../prettify_sa.css" type="text/css"><script type="text/javascript" src="../prettify.js"></script></head><body onload="prettyPrint()"><pre class="prettyprint"><pre><span id='Ext-chart.series.Line'>/**
+</span> * @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',
+
+<span id='Ext-chart.series.Line-cfg-selectionTolerance'> /**
+</span> * @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,
+
+<span id='Ext-chart.series.Line-cfg-showMarkers'> /**
+</span> * @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,
+
+<span id='Ext-chart.series.Line-cfg-markerConfig'> /**
+</span> * @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:
+ *
+ <pre><code>
+ markerConfig: {
+ type: 'circle',
+ radius: 4,
+ 'fill': '#f00'
+ }
+ </code></pre>
+
+ */
+ markerConfig: {},
+
+<span id='Ext-chart.series.Line-cfg-style'> /**
+</span> * @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: {},
+
+<span id='Ext-chart.series.Line-cfg-smooth'> /**
+</span> * @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,
+
+<span id='Ext-chart.series.Line-cfg-fill'> /**
+</span> * @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
+ };
+ },
+
+<span id='Ext-chart.series.Line-method-drawSeries'> /**
+</span> * 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)) {
+ //<debug warn>
+ 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);
+ }
+ //</debug>
+ 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);
+ }
+});</pre></pre></body></html>
\ No newline at end of file