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

    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 markerCfg 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', requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shapes', 'Ext.draw.Draw', 'Ext.fx.Anim'], /* End Definitions */ type: '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 date points along the line. If true, * then the {@link #markerCfg} config item will determine the markers' styling. */ showMarkers: true,
/** * @cfg {Object} markerCfg * The display style for the markers. Only used if {@link #showMarkers} is true. */ markerCfg: {},
/** * @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 {String} dash * Optional dash array for the line. */ dash: '',
/** * @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: { lineWidth: 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, shadowGroup, shadowAttributes = this.shadowAttributes, shadowBarAttr, shadows, shadow, shindex, lnsh = shadowGroups.length, dummyPath = ["M"], path = ["M"], fromPath, fill, fillPath, rendererAttributes, x, y, prevX, prevY, markerIndex = chart.markerIndex, firstY, markerCount, i, j, ln, axis, ends, marker, markerAux, item, xValue, yValue, coords, xValues = [], yValues = [], xScale, yScale, minX, maxX, minY, maxY, line, onbreak = false, animation, markerStyle = me.markerStyle, seriesStyle = me.seriesStyle, seriesLabelStyle = me.seriesLabelStyle, colorArrayStyle = me.colorArrayStyle, colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0, seriesIdx = me.seriesIdx, endMarkerStyle, endLineStyle, type, props, firstMarker; //prepare style objects for line and markers endMarkerStyle = Ext.apply(markerStyle, me.markerCfg); 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 } }); } } me.unHighlightItem(); me.cleanHighlights(); //get box dimensions 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; } } 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); // Ensure a value if (typeof xValue == 'string' || typeof xValue == 'object') { xValue = i; } if (typeof yValue == 'string') { yValue = i; } xValues.push(xValue); yValues.push(yValue); }, me); ln = xValues.length; if (ln > bbox.width || ln > bbox.height) { coords = me.shrink(xValues, yValues, Math.min(bbox.width, bbox.height)); 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; 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.Shapes[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 (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) { rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store); me.onAnimate(item, { to: Ext.apply(rendererAttributes, endMarkerStyle || {}) }); } } 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 }); } } if (me.fill) { me.fillPath.setAttributes({ path: fillPath }, true); } if (showMarkers) { for(i = 0; i < ln; i++) { item = markerGroup.getAt(i); if (item) { rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store); item.setAttributes(Ext.apply(endMarkerStyle || {}, rendererAttributes || {}), true); // me.onAnimate(item, { // to: Ext.apply(rendererAttributes, endMarkerStyle || {}) // }); } } for(; i < markerCount; i++) { item = markerGroup.getAt(i); item.hide(true); } // for(i = 0; i < (chart.markerIndex || 0); 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 = new 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 = new 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) { 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, dir, norm, normal, a, aprev, anext, bbox = callout.label.getBBox(), offsetFromViz = 30, offsetToSide = 10, offsetBox = 3, boxx, boxy, boxw, boxh, p, clipRect = me.clipRect, 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 box position me.onAnimate(callout.box, { to: { x: boxx, y: boxy, width: boxw, height: boxh } }); //set text position me.onAnimate(callout.label, { to: { x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)), y: y } }); } 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); } },
/** * For a given x/y point relative to the Surface, find a corresponding item from this * series, if any. * * For Line series, this snaps to the nearest vertex if the target point is within a * certain vertical distance from the line. * * @param {Number} x * @param {Number} y * @return {Object} */ getItemForPoint: function(x, y) { var me = this, items = me.items, tolerance = me.selectionTolerance, result = null, prevItem, nextItem, prevPoint, nextPoint, i, ln, x1, y1, x2, y2, xIntersect, yIntersect, dist1, dist2, dist, midx, midy, sqrt = Math.sqrt, abs = Math.abs; if (items && items.length) { // Find coordinates for the vertices before and after the target point for (i = 0, ln = items.length; i < ln; i++) { if (items[i].point[0] >= x) { nextItem = items[i]; prevItem = i && items[i - 1]; break; } } 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; } /*Doesn't work properly. Fix for later. // Determine whether the point is within the vertical tolerance distance from a straight // line between the two vertices (TODO make this accurate for curved lines) if (x >= x1 && x <= x2) { yIntersect = (y2 - y1) / (x2 - x1) * (x - x1) + y1; console.log('x', x, 'y', y, 'yIntersect', yIntersect, 'y1', y1, 'y2', y2, 'x1', x1, 'x2', x2); if (Math.abs(yIntersect - y) <= tolerance) { result = (x2 - x < x - x1) ? nextItem : prevItem; } } */ } return result; }, // @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); } });