2 * @class Ext.chart.series.Line
3 * @extends Ext.chart.series.Cartesian
6 Creates a Line Chart. A Line Chart is a useful visualization technique to display quantitative information for different
7 categories or other real values (as opposed to the bar chart), that can show some progression (or regression) in the dataset.
8 As with all other series, the Line Series must be appended in the *series* Chart array configuration. See the Chart
9 documentation for more information. A typical configuration object for the line series could be:
11 {@img Ext.chart.series.Line/Ext.chart.series.Line.png Ext.chart.series.Line chart series}
13 var store = Ext.create('Ext.data.JsonStore', {
14 fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
16 {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
17 {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
18 {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
19 {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
20 {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
24 Ext.create('Ext.chart.Chart', {
25 renderTo: Ext.getBody(),
35 renderer: Ext.util.Format.numberRenderer('0,0')
37 title: 'Sample Values',
44 title: 'Sample Metrics'
82 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
83 `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
84 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
85 when hovered. The second series has `fill=true` which means that the line will also have an area below it of the same color.
89 Ext.define('Ext.chart.series.Line', {
91 /* Begin Definitions */
93 extend: 'Ext.chart.series.Cartesian',
95 alternateClassName: ['Ext.chart.LineSeries', 'Ext.chart.LineChart'],
97 requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shape', 'Ext.draw.Draw', 'Ext.fx.Anim'],
103 alias: 'series.line',
106 * @cfg {Number} selectionTolerance
107 * The offset distance from the cursor position to the line series to trigger events (then used for highlighting series, etc).
109 selectionTolerance: 20,
112 * @cfg {Boolean} showMarkers
113 * Whether markers should be displayed at the data points along the line. If true,
114 * then the {@link #markerConfig} config item will determine the markers' styling.
119 * @cfg {Object} markerConfig
120 * The display style for the markers. Only used if {@link #showMarkers} is true.
121 * The markerConfig is a configuration object containing the same set of properties defined in
122 * the Sprite class. For example, if we were to set red circles as markers to the line series we could
137 * @cfg {Object} style
138 * An object containing styles for the visualization lines. These styles will override the theme styles.
139 * Some options contained within the style object will are described next.
144 * @cfg {Boolean} smooth
145 * If true, the line will be smoothed/rounded around its points, otherwise straight line
146 * segments will be drawn. Defaults to false.
151 * @cfg {Boolean} fill
152 * If true, the area below the line will be filled in using the {@link #style.eefill} and
153 * {@link #style.opacity} config properties. Defaults to false.
157 constructor: function(config) {
158 this.callParent(arguments);
160 surface = me.chart.surface,
161 shadow = me.chart.shadow,
163 Ext.apply(me, config, {
169 "stroke-opacity": 0.05,
170 stroke: 'rgb(0, 0, 0)',
177 "stroke-opacity": 0.1,
178 stroke: 'rgb(0, 0, 0)',
185 "stroke-opacity": 0.15,
186 stroke: 'rgb(0, 0, 0)',
193 me.group = surface.getGroup(me.seriesId);
194 if (me.showMarkers) {
195 me.markerGroup = surface.getGroup(me.seriesId + '-markers');
198 for (i = 0, l = this.shadowAttributes.length; i < l; i++) {
199 me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
204 // @private makes an average of points when there are more data points than pixels to be rendered.
205 shrink: function(xValues, yValues, size) {
206 // Start at the 2nd point...
207 var len = xValues.length,
208 ratio = Math.floor(len / size),
215 for (; i < len; ++i) {
216 xSum += xValues[i] || 0;
217 ySum += yValues[i] || 0;
218 if (i % ratio == 0) {
219 xRes.push(xSum/ratio);
220 yRes.push(ySum/ratio);
232 * Draws the series for the current chart.
234 drawSeries: function() {
237 store = chart.substore || chart.store,
238 surface = chart.surface,
239 chartBBox = chart.chartBBox,
242 gutterX = chart.maxGutter[0],
243 gutterY = chart.maxGutter[1],
244 showMarkers = me.showMarkers,
245 markerGroup = me.markerGroup,
246 enableShadows = chart.shadow,
247 shadowGroups = me.shadowGroups,
248 shadowAttributes = this.shadowAttributes,
249 lnsh = shadowGroups.length,
252 markerIndex = chart.markerIndex,
253 axes = [].concat(me.axis),
259 markerStyle = me.markerStyle,
260 seriesStyle = me.seriesStyle,
261 seriesLabelStyle = me.seriesLabelStyle,
262 colorArrayStyle = me.colorArrayStyle,
263 colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
264 seriesIdx = me.seriesIdx, shadows, shadow, shindex, fromPath, fill, fillPath, rendererAttributes,
265 x, y, prevX, prevY, firstY, markerCount, i, j, ln, axis, ends, marker, markerAux, item, xValue,
266 yValue, coords, xScale, yScale, minX, maxX, minY, maxY, line, animation, endMarkerStyle,
267 endLineStyle, type, props, firstMarker;
269 //if store is empty then there's nothing to draw.
270 if (!store || !store.getCount()) {
274 //prepare style objects for line and markers
275 endMarkerStyle = Ext.apply(markerStyle, me.markerConfig);
276 type = endMarkerStyle.type;
277 delete endMarkerStyle.type;
278 endLineStyle = Ext.apply(seriesStyle, me.style);
279 //if no stroke with is specified force it to 0.5 because this is
280 //about making *lines*
281 if (!endLineStyle['stroke-width']) {
282 endLineStyle['stroke-width'] = 0.5;
284 //If we're using a time axis and we need to translate the points,
285 //then reuse the first markers as the last markers.
286 if (markerIndex && markerGroup && markerGroup.getCount()) {
287 for (i = 0; i < markerIndex; i++) {
288 marker = markerGroup.getAt(i);
289 markerGroup.remove(marker);
290 markerGroup.add(marker);
291 markerAux = markerGroup.getAt(markerGroup.getCount() - 2);
292 marker.setAttributes({
296 x: markerAux.attr.translation.x,
297 y: markerAux.attr.translation.y
303 me.unHighlightItem();
304 me.cleanHighlights();
309 me.clipRect = [bbox.x, bbox.y, bbox.width, bbox.height];
311 for (i = 0, ln = axes.length; i < ln; i++) {
312 axis = chart.axes.get(axes[i]);
314 ends = axis.calcEnds();
315 if (axis.position == 'top' || axis.position == 'bottom') {
325 // If a field was specified without a corresponding axis, create one to get bounds
326 //only do this for the axis where real values are bound (that's why we check for
328 if (me.xField && !Ext.isNumber(minX)
329 && (me.axis == 'bottom' || me.axis == 'top')) {
330 axis = Ext.create('Ext.chart.axis.Axis', {
332 fields: [].concat(me.xField)
337 if (me.yField && !Ext.isNumber(minY)
338 && (me.axis == 'right' || me.axis == 'left')) {
339 axis = Ext.create('Ext.chart.axis.Axis', {
341 fields: [].concat(me.yField)
349 xScale = bbox.width / (store.getCount() - 1);
352 xScale = bbox.width / (maxX - minX);
357 yScale = bbox.height / (store.getCount() - 1);
360 yScale = bbox.height / (maxY - minY);
363 store.each(function(record, i) {
364 xValue = record.get(me.xField);
365 yValue = record.get(me.yField);
366 //skip undefined values
367 if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) {
369 if (Ext.isDefined(Ext.global.console)) {
370 Ext.global.console.warn("[Ext.chart.series.Line] Skipping a store element with an undefined value at ", record, xValue, yValue);
376 if (typeof xValue == 'string' || typeof xValue == 'object'
377 //set as uniform distribution if the axis is a category axis.
378 || (me.axis != 'top' && me.axis != 'bottom')) {
381 if (typeof yValue == 'string' || typeof yValue == 'object'
382 //set as uniform distribution if the axis is a category axis.
383 || (me.axis != 'left' && me.axis != 'right')) {
386 xValues.push(xValue);
387 yValues.push(yValue);
391 if (ln > bbox.width) {
392 coords = me.shrink(xValues, yValues, bbox.width);
400 for (i = 0; i < ln; i++) {
403 if (yValue === false) {
404 if (path.length == 1) {
408 me.items.push(false);
411 x = (bbox.x + (xValue - minX) * xScale).toFixed(2);
412 y = ((bbox.y + bbox.height) - (yValue - minY) * yScale).toFixed(2);
417 path = path.concat([x, y]);
419 if ((typeof firstY == 'undefined') && (typeof y != 'undefined')) {
422 // If this is the first line, create a dummypath to animate in from.
423 if (!me.line || chart.resizing) {
424 dummyPath = dummyPath.concat([x, bbox.y + bbox.height / 2]);
427 // When resizing, reset before animating
428 if (chart.animate && chart.resizing && me.line) {
429 me.line.setAttributes({
433 me.fillPath.setAttributes({
438 if (me.line.shadows) {
439 shadows = me.line.shadows;
440 for (j = 0, lnsh = shadows.length; j < lnsh; j++) {
442 shadow.setAttributes({
449 marker = markerGroup.getAt(i);
451 marker = Ext.chart.Shape[type](surface, Ext.apply({
452 group: [group, markerGroup],
456 y: prevY || (bbox.y + bbox.height / 2)
458 value: '"' + xValue + ', ' + yValue + '"'
467 marker.setAttributes({
468 value: '"' + xValue + ', ' + yValue + '"',
481 value: [xValue, yValue],
484 storeItem: store.getAt(i)
490 if (path.length <= 1) {
491 //nothing to be rendered
496 path = Ext.draw.Draw.smooth(path, 6);
499 //Correct path if we're animating timeAxis intervals
500 if (chart.markerIndex && me.previousPath) {
501 fromPath = me.previousPath;
502 fromPath.splice(1, 2);
507 // Only create a line if one doesn't exist.
509 me.line = surface.add(Ext.apply({
513 stroke: endLineStyle.stroke || endLineStyle.fill
514 }, endLineStyle || {}));
515 //unset fill here (there's always a default fill withing the themes).
516 me.line.setAttributes({
519 if (!endLineStyle.stroke && colorArrayLength) {
520 me.line.setAttributes({
521 stroke: colorArrayStyle[seriesIdx % colorArrayLength]
526 shadows = me.line.shadows = [];
527 for (shindex = 0; shindex < lnsh; shindex++) {
528 shadowBarAttr = shadowAttributes[shindex];
529 shadowBarAttr = Ext.apply({}, shadowBarAttr, { path: dummyPath });
530 shadow = chart.surface.add(Ext.apply({}, {
532 group: shadowGroups[shindex]
534 shadows.push(shadow);
539 fillPath = path.concat([
540 ["L", x, bbox.y + bbox.height],
541 ["L", bbox.x, bbox.y + bbox.height],
542 ["L", bbox.x, firstY]
545 me.fillPath = surface.add({
548 opacity: endLineStyle.opacity || 0.3,
549 fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill,
554 markerCount = showMarkers && markerGroup.getCount();
558 //Add renderer to line. There is not unique record associated with this.
559 rendererAttributes = me.renderer(line, false, { path: path }, i, store);
560 Ext.apply(rendererAttributes, endLineStyle || {}, {
561 stroke: endLineStyle.stroke || endLineStyle.fill
563 //fill should not be used here but when drawing the special fill path object
564 delete rendererAttributes.fill;
565 if (chart.markerIndex && me.previousPath) {
566 me.animation = animation = me.onAnimate(line, {
567 to: rendererAttributes,
573 me.animation = animation = me.onAnimate(line, {
574 to: rendererAttributes
579 shadows = line.shadows;
580 for(j = 0; j < lnsh; j++) {
581 if (chart.markerIndex && me.previousPath) {
582 me.onAnimate(shadows[j], {
584 from: { path: fromPath }
587 me.onAnimate(shadows[j], {
595 me.onAnimate(me.fillPath, {
598 fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill
599 }, endLineStyle || {})
604 for(i = 0; i < ln; i++) {
605 item = markerGroup.getAt(i);
608 rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store);
610 to: Ext.apply(rendererAttributes, endMarkerStyle || {})
613 item.setAttributes(Ext.apply({
619 for(; i < markerCount; i++) {
620 item = markerGroup.getAt(i);
623 // for(i = 0; i < (chart.markerIndex || 0)-1; i++) {
624 // item = markerGroup.getAt(i);
629 rendererAttributes = me.renderer(me.line, false, { path: path, hidden: false }, i, store);
630 Ext.apply(rendererAttributes, endLineStyle || {}, {
631 stroke: endLineStyle.stroke || endLineStyle.fill
633 //fill should not be used here but when drawing the special fill path object
634 delete rendererAttributes.fill;
635 me.line.setAttributes(rendererAttributes, true);
636 //set path for shadows
638 shadows = me.line.shadows;
639 for(j = 0; j < lnsh; j++) {
640 shadows[j].setAttributes({
646 me.fillPath.setAttributes({
651 for(i = 0; i < ln; i++) {
652 item = markerGroup.getAt(i);
655 rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store);
656 item.setAttributes(Ext.apply(endMarkerStyle || {}, rendererAttributes || {}), true);
662 for(; i < markerCount; i++) {
663 item = markerGroup.getAt(i);
669 if (chart.markerIndex) {
670 path.splice(1, 0, path[1], path[2]);
671 me.previousPath = path;
677 // @private called when a label is to be created.
678 onCreateLabel: function(storeItem, item, i, display) {
680 group = me.labelsGroup,
683 endLabelStyle = Ext.apply(config, me.seriesLabelStyle);
685 return me.chart.surface.add(Ext.apply({
687 'text-anchor': 'middle',
690 'y': bbox.y + bbox.height / 2
691 }, endLabelStyle || {}));
694 // @private called when a label is to be created.
695 onPlaceLabel: function(label, storeItem, item, i, display, animate) {
698 resizing = chart.resizing,
700 format = config.renderer,
701 field = config.field,
705 radius = item.sprite.attr.radius,
708 label.setAttributes({
709 text: format(storeItem.get(field)),
713 if (display == 'rotate') {
714 label.setAttributes({
715 'text-anchor': 'start',
722 //correct label position to fit into the box
723 bb = label.getBBox();
726 x = x < bbox.x? bbox.x : x;
727 x = (x + width > bbox.x + bbox.width)? (x - (x + width - bbox.x - bbox.width)) : x;
728 y = (y - height < bbox.y)? bbox.y + height : y;
730 } else if (display == 'under' || display == 'over') {
731 //TODO(nicolas): find out why width/height values in circle bounding boxes are undefined.
732 bb = item.sprite.getBBox();
733 bb.width = bb.width || (radius * 2);
734 bb.height = bb.height || (radius * 2);
735 y = y + (display == 'over'? -bb.height : bb.height);
736 //correct label position to fit into the box
737 bb = label.getBBox();
739 height = bb.height/2;
740 x = x - width < bbox.x? bbox.x + width : x;
741 x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
742 y = y - height < bbox.y? bbox.y + height : y;
743 y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
746 if (me.chart.animate && !me.chart.resizing) {
748 me.onAnimate(label, {
755 label.setAttributes({
760 me.animation.on('afteranimate', function() {
769 //@private Overriding highlights.js highlightItem method.
770 highlightItem: function() {
772 me.callParent(arguments);
773 if (this.line && !this.highlighted) {
774 if (!('__strokeWidth' in this.line)) {
775 this.line.__strokeWidth = this.line.attr['stroke-width'] || 0;
777 if (this.line.__anim) {
778 this.line.__anim.paused = true;
780 this.line.__anim = Ext.create('Ext.fx.Anim', {
783 'stroke-width': this.line.__strokeWidth + 3
786 this.highlighted = true;
790 //@private Overriding highlights.js unHighlightItem method.
791 unHighlightItem: function() {
793 me.callParent(arguments);
794 if (this.line && this.highlighted) {
795 this.line.__anim = Ext.create('Ext.fx.Anim', {
798 'stroke-width': this.line.__strokeWidth
801 this.highlighted = false;
805 //@private called when a callout needs to be placed.
806 onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) {
813 surface = chart.surface,
814 resizing = chart.resizing,
815 config = me.callouts,
817 prev = i == 0? false : items[i -1].point,
818 next = (i == items.length -1)? false : items[i +1].point,
819 cur = [+item.point[0], +item.point[1]],
820 dir, norm, normal, a, aprev, anext,
821 offsetFromViz = config.offsetFromViz || 30,
822 offsetToSide = config.offsetToSide || 10,
823 offsetBox = config.offsetBox || 3,
824 boxx, boxy, boxw, boxh,
825 p, clipRect = me.clipRect,
827 width: config.styles.width || 10,
828 height: config.styles.height || 10
832 //get the right two points
839 a = (next[1] - prev[1]) / (next[0] - prev[0]);
840 aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]);
841 anext = (next[1] - cur[1]) / (next[0] - cur[0]);
843 norm = Math.sqrt(1 + a * a);
844 dir = [1 / norm, a / norm];
845 normal = [-dir[1], dir[0]];
847 //keep the label always on the outer part of the "elbow"
848 if (aprev > 0 && anext < 0 && normal[1] < 0
849 || aprev < 0 && anext > 0 && normal[1] > 0) {
852 } else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0
853 || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) {
858 x = cur[0] + normal[0] * offsetFromViz;
859 y = cur[1] + normal[1] * offsetFromViz;
861 //box position and dimensions
862 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
863 boxy = y - bbox.height /2 - offsetBox;
864 boxw = bbox.width + 2 * offsetBox;
865 boxh = bbox.height + 2 * offsetBox;
867 //now check if we're out of bounds and invert the normal vector correspondingly
868 //this may add new overlaps between labels (but labels won't be out of bounds).
869 if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) {
872 if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) {
877 x = cur[0] + normal[0] * offsetFromViz;
878 y = cur[1] + normal[1] * offsetFromViz;
880 //update box position and dimensions
881 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
882 boxy = y - bbox.height /2 - offsetBox;
883 boxw = bbox.width + 2 * offsetBox;
884 boxh = bbox.height + 2 * offsetBox;
887 //set the line from the middle of the pie to the box.
888 me.onAnimate(callout.lines, {
890 path: ["M", cur[0], cur[1], "L", x, y, "Z"]
893 //set component position
895 callout.panel.setPosition(boxx, boxy, true);
899 //set the line from the middle of the pie to the box.
900 callout.lines.setAttributes({
901 path: ["M", cur[0], cur[1], "L", x, y, "Z"]
903 //set component position
905 callout.panel.setPosition(boxx, boxy);
909 callout[p].show(true);
913 isItemInPoint: function(x, y, item, i) {
916 tolerance = me.selectionTolerance,
929 dist1, dist2, dist, midx, midy,
930 sqrt = Math.sqrt, abs = Math.abs;
933 prevItem = i && items[i - 1];
936 prevItem = items[ln - 1];
938 prevPoint = prevItem && prevItem.point;
939 nextPoint = nextItem && nextItem.point;
940 x1 = prevItem ? prevPoint[0] : nextPoint[0] - tolerance;
941 y1 = prevItem ? prevPoint[1] : nextPoint[1];
942 x2 = nextItem ? nextPoint[0] : prevPoint[0] + tolerance;
943 y2 = nextItem ? nextPoint[1] : prevPoint[1];
944 dist1 = sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1));
945 dist2 = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2));
946 dist = Math.min(dist1, dist2);
948 if (dist <= tolerance) {
949 return dist == dist1? prevItem : nextItem;
954 // @private toggle visibility of all series elements (markers, sprites).
955 toggleAll: function(show) {
957 i, ln, shadow, shadows;
959 Ext.chart.series.Line.superclass.hideAll.call(me);
962 Ext.chart.series.Line.superclass.showAll.call(me);
965 me.line.setAttributes({
969 if (me.line.shadows) {
970 for (i = 0, shadows = me.line.shadows, ln = shadows.length; i < ln; i++) {
972 shadow.setAttributes({
979 me.fillPath.setAttributes({
985 // @private hide all series elements (markers, sprites).
986 hideAll: function() {
987 this.toggleAll(false);
990 // @private hide all series elements (markers, sprites).
991 showAll: function() {
992 this.toggleAll(true);