2 * @class Ext.chart.series.Line
3 * @extends Ext.chart.series.Cartesian
5 * Creates a Line Chart. A Line Chart is a useful visualization technique to display quantitative information for different
6 * categories or other real values (as opposed to the bar chart), that can show some progression (or regression) in the dataset.
7 * As with all other series, the Line Series must be appended in the *series* Chart array configuration. See the Chart
8 * documentation for more information. A typical configuration object for the line series could be:
10 * {@img Ext.chart.series.Line/Ext.chart.series.Line.png Ext.chart.series.Line chart series}
12 * var store = Ext.create('Ext.data.JsonStore', {
13 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
15 * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
16 * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
17 * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
18 * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
19 * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
23 * Ext.create('Ext.chart.Chart', {
24 * renderTo: Ext.getBody(),
34 * renderer: Ext.util.Format.numberRenderer('0,0')
36 * title: 'Sample Values',
43 * title: 'Sample Metrics'
79 * In this configuration we're adding two series (or lines), one bound to the `data1`
80 * property of the store and the other to `data3`. The type for both configurations is
81 * `line`. The `xField` for both series is the same, the name propert of the store.
82 * Both line series share the same axis, the left axis. You can set particular marker
83 * configuration by adding properties onto the markerConfig object. Both series have
84 * 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
86 * have an area below it of the same color.
88 * **Note:** In the series definition remember to explicitly set the axis to bind the
89 * values of the line series to. This can be done by using the `axis` configuration property.
91 Ext.define('Ext.chart.series.Line', {
93 /* Begin Definitions */
95 extend: 'Ext.chart.series.Cartesian',
97 alternateClassName: ['Ext.chart.LineSeries', 'Ext.chart.LineChart'],
99 requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shape', 'Ext.draw.Draw', 'Ext.fx.Anim'],
101 /* End Definitions */
105 alias: 'series.line',
109 * The position of the axis to bind the values to. Possible values are 'left', 'bottom', 'top' and 'right'.
110 * You must explicitly set this value to bind the values of the line series to the ones in the axis, otherwise a
111 * relative scale will be used.
115 * @cfg {Number} selectionTolerance
116 * The offset distance from the cursor position to the line series to trigger events (then used for highlighting series, etc).
118 selectionTolerance: 20,
121 * @cfg {Boolean} showMarkers
122 * Whether markers should be displayed at the data points along the line. If true,
123 * then the {@link #markerConfig} config item will determine the markers' styling.
128 * @cfg {Object} markerConfig
129 * The display style for the markers. Only used if {@link #showMarkers} is true.
130 * The markerConfig is a configuration object containing the same set of properties defined in
131 * the Sprite class. For example, if we were to set red circles as markers to the line series we could
146 * @cfg {Object} style
147 * An object containing styles for the visualization lines. These styles will override the theme styles.
148 * Some options contained within the style object will are described next.
153 * @cfg {Boolean} smooth
154 * If true, the line will be smoothed/rounded around its points, otherwise straight line
155 * segments will be drawn. Defaults to false.
160 * @cfg {Boolean} fill
161 * If true, the area below the line will be filled in using the {@link #style.eefill} and
162 * {@link #style.opacity} config properties. Defaults to false.
166 constructor: function(config) {
167 this.callParent(arguments);
169 surface = me.chart.surface,
170 shadow = me.chart.shadow,
172 Ext.apply(me, config, {
178 "stroke-opacity": 0.05,
179 stroke: 'rgb(0, 0, 0)',
186 "stroke-opacity": 0.1,
187 stroke: 'rgb(0, 0, 0)',
194 "stroke-opacity": 0.15,
195 stroke: 'rgb(0, 0, 0)',
202 me.group = surface.getGroup(me.seriesId);
203 if (me.showMarkers) {
204 me.markerGroup = surface.getGroup(me.seriesId + '-markers');
207 for (i = 0, l = this.shadowAttributes.length; i < l; i++) {
208 me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
213 // @private makes an average of points when there are more data points than pixels to be rendered.
214 shrink: function(xValues, yValues, size) {
215 // Start at the 2nd point...
216 var len = xValues.length,
217 ratio = Math.floor(len / size),
224 for (; i < len; ++i) {
225 xSum += xValues[i] || 0;
226 ySum += yValues[i] || 0;
227 if (i % ratio == 0) {
228 xRes.push(xSum/ratio);
229 yRes.push(ySum/ratio);
241 * Draws the series for the current chart.
243 drawSeries: function() {
246 store = chart.substore || chart.store,
247 surface = chart.surface,
248 chartBBox = chart.chartBBox,
251 gutterX = chart.maxGutter[0],
252 gutterY = chart.maxGutter[1],
253 showMarkers = me.showMarkers,
254 markerGroup = me.markerGroup,
255 enableShadows = chart.shadow,
256 shadowGroups = me.shadowGroups,
257 shadowAttributes = this.shadowAttributes,
258 lnsh = shadowGroups.length,
261 markerIndex = chart.markerIndex,
262 axes = [].concat(me.axis),
270 markerStyle = me.markerStyle,
271 seriesStyle = me.seriesStyle,
272 seriesLabelStyle = me.seriesLabelStyle,
273 colorArrayStyle = me.colorArrayStyle,
274 colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
281 seriesIdx = me.seriesIdx, shadows, shadow, shindex, fromPath, fill, fillPath, rendererAttributes,
282 x, y, prevX, prevY, firstY, markerCount, i, j, ln, axis, ends, marker, markerAux, item, xValue,
283 yValue, coords, xScale, yScale, minX, maxX, minY, maxY, line, animation, endMarkerStyle,
284 endLineStyle, type, props, firstMarker, count;
286 //if store is empty then there's nothing to draw.
287 if (!store || !store.getCount()) {
291 //prepare style objects for line and markers
292 endMarkerStyle = Ext.apply(markerStyle, me.markerConfig);
293 type = endMarkerStyle.type;
294 delete endMarkerStyle.type;
295 endLineStyle = Ext.apply(seriesStyle, me.style);
296 //if no stroke with is specified force it to 0.5 because this is
297 //about making *lines*
298 if (!endLineStyle['stroke-width']) {
299 endLineStyle['stroke-width'] = 0.5;
301 //If we're using a time axis and we need to translate the points,
302 //then reuse the first markers as the last markers.
303 if (markerIndex && markerGroup && markerGroup.getCount()) {
304 for (i = 0; i < markerIndex; i++) {
305 marker = markerGroup.getAt(i);
306 markerGroup.remove(marker);
307 markerGroup.add(marker);
308 markerAux = markerGroup.getAt(markerGroup.getCount() - 2);
309 marker.setAttributes({
313 x: markerAux.attr.translation.x,
314 y: markerAux.attr.translation.y
320 me.unHighlightItem();
321 me.cleanHighlights();
326 me.clipRect = [bbox.x, bbox.y, bbox.width, bbox.height];
328 chart.axes.each(function(axis) {
329 //only apply position calculations to axes that affect this series
330 //this means the axis in the position referred by this series and also
331 //the axis in the other coordinate for this series. For example: (left, top|bottom),
332 //or (top, left|right), etc.
333 if (axis.position == me.axis || axis.position != posHash[me.axis]) {
335 if (axis.type != 'Numeric') {
339 numericAxis = (numericAxis && axis.type == 'Numeric');
341 ends = axis.calcEnds();
342 if (axis.position == 'top' || axis.position == 'bottom') {
354 //If there's only one axis specified for a series, then we set the default type of the other
355 //axis to a category axis. So in this case numericAxis, which would be true if both axes affecting
356 //the series are numeric should be false.
357 if (numericAxis && axisCount == 1) {
361 // If a field was specified without a corresponding axis, create one to get bounds
362 //only do this for the axis where real values are bound (that's why we check for
364 if (me.xField && !Ext.isNumber(minX)) {
365 if (me.axis == 'bottom' || me.axis == 'top') {
366 axis = Ext.create('Ext.chart.axis.Axis', {
368 fields: [].concat(me.xField)
372 } else if (numericAxis) {
373 axis = Ext.create('Ext.chart.axis.Axis', {
375 fields: [].concat(me.xField),
383 if (me.yField && !Ext.isNumber(minY)) {
384 if (me.axis == 'right' || me.axis == 'left') {
385 axis = Ext.create('Ext.chart.axis.Axis', {
387 fields: [].concat(me.yField)
391 } else if (numericAxis) {
392 axis = Ext.create('Ext.chart.axis.Axis', {
394 fields: [].concat(me.yField),
404 xScale = bbox.width / (store.getCount() - 1);
407 xScale = bbox.width / (maxX - minX);
412 yScale = bbox.height / (store.getCount() - 1);
415 yScale = bbox.height / (maxY - minY);
418 store.each(function(record, i) {
419 xValue = record.get(me.xField);
420 yValue = record.get(me.yField);
421 //skip undefined values
422 if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) {
424 if (Ext.isDefined(Ext.global.console)) {
425 Ext.global.console.warn("[Ext.chart.series.Line] Skipping a store element with an undefined value at ", record, xValue, yValue);
431 if (typeof xValue == 'string' || typeof xValue == 'object'
432 //set as uniform distribution if the axis is a category axis.
433 || (me.axis != 'top' && me.axis != 'bottom' && !numericAxis)) {
436 if (typeof yValue == 'string' || typeof yValue == 'object'
437 //set as uniform distribution if the axis is a category axis.
438 || (me.axis != 'left' && me.axis != 'right' && !numericAxis)) {
441 xValues.push(xValue);
442 yValues.push(yValue);
446 if (ln > bbox.width) {
447 coords = me.shrink(xValues, yValues, bbox.width);
456 for (i = 0; i < ln; i++) {
459 if (yValue === false) {
460 if (path.length == 1) {
464 me.items.push(false);
467 x = (bbox.x + (xValue - minX) * xScale).toFixed(2);
468 y = ((bbox.y + bbox.height) - (yValue - minY) * yScale).toFixed(2);
473 path = path.concat([x, y]);
475 if ((typeof firstY == 'undefined') && (typeof y != 'undefined')) {
478 // If this is the first line, create a dummypath to animate in from.
479 if (!me.line || chart.resizing) {
480 dummyPath = dummyPath.concat([x, bbox.y + bbox.height / 2]);
483 // When resizing, reset before animating
484 if (chart.animate && chart.resizing && me.line) {
485 me.line.setAttributes({
489 me.fillPath.setAttributes({
494 if (me.line.shadows) {
495 shadows = me.line.shadows;
496 for (j = 0, lnsh = shadows.length; j < lnsh; j++) {
498 shadow.setAttributes({
505 marker = markerGroup.getAt(count++);
507 marker = Ext.chart.Shape[type](surface, Ext.apply({
508 group: [group, markerGroup],
512 y: prevY || (bbox.y + bbox.height / 2)
514 value: '"' + xValue + ', ' + yValue + '"'
523 marker.setAttributes({
524 value: '"' + xValue + ', ' + yValue + '"',
537 value: [xValue, yValue],
540 storeItem: store.getAt(i)
546 if (path.length <= 1) {
547 //nothing to be rendered
552 path = Ext.draw.Draw.smooth(path, 6);
555 //Correct path if we're animating timeAxis intervals
556 if (chart.markerIndex && me.previousPath) {
557 fromPath = me.previousPath;
558 fromPath.splice(1, 2);
563 // Only create a line if one doesn't exist.
565 me.line = surface.add(Ext.apply({
569 stroke: endLineStyle.stroke || endLineStyle.fill
570 }, endLineStyle || {}));
571 //unset fill here (there's always a default fill withing the themes).
572 me.line.setAttributes({
575 if (!endLineStyle.stroke && colorArrayLength) {
576 me.line.setAttributes({
577 stroke: colorArrayStyle[seriesIdx % colorArrayLength]
582 shadows = me.line.shadows = [];
583 for (shindex = 0; shindex < lnsh; shindex++) {
584 shadowBarAttr = shadowAttributes[shindex];
585 shadowBarAttr = Ext.apply({}, shadowBarAttr, { path: dummyPath });
586 shadow = chart.surface.add(Ext.apply({}, {
588 group: shadowGroups[shindex]
590 shadows.push(shadow);
595 fillPath = path.concat([
596 ["L", x, bbox.y + bbox.height],
597 ["L", bbox.x, bbox.y + bbox.height],
598 ["L", bbox.x, firstY]
601 me.fillPath = surface.add({
604 opacity: endLineStyle.opacity || 0.3,
605 fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill,
610 markerCount = showMarkers && markerGroup.getCount();
614 //Add renderer to line. There is not unique record associated with this.
615 rendererAttributes = me.renderer(line, false, { path: path }, i, store);
616 Ext.apply(rendererAttributes, endLineStyle || {}, {
617 stroke: endLineStyle.stroke || endLineStyle.fill
619 //fill should not be used here but when drawing the special fill path object
620 delete rendererAttributes.fill;
621 if (chart.markerIndex && me.previousPath) {
622 me.animation = animation = me.onAnimate(line, {
623 to: rendererAttributes,
629 me.animation = animation = me.onAnimate(line, {
630 to: rendererAttributes
635 shadows = line.shadows;
636 for(j = 0; j < lnsh; j++) {
637 if (chart.markerIndex && me.previousPath) {
638 me.onAnimate(shadows[j], {
640 from: { path: fromPath }
643 me.onAnimate(shadows[j], {
651 me.onAnimate(me.fillPath, {
654 fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill
655 }, endLineStyle || {})
661 for(i = 0; i < ln; i++) {
663 item = markerGroup.getAt(count++);
665 rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store);
667 to: Ext.apply(rendererAttributes, endMarkerStyle || {})
672 for(; count < markerCount; count++) {
673 item = markerGroup.getAt(count);
678 rendererAttributes = me.renderer(me.line, false, { path: path, hidden: false }, i, store);
679 Ext.apply(rendererAttributes, endLineStyle || {}, {
680 stroke: endLineStyle.stroke || endLineStyle.fill
682 //fill should not be used here but when drawing the special fill path object
683 delete rendererAttributes.fill;
684 me.line.setAttributes(rendererAttributes, true);
685 //set path for shadows
687 shadows = me.line.shadows;
688 for(j = 0; j < lnsh; j++) {
689 shadows[j].setAttributes({
695 me.fillPath.setAttributes({
701 for(i = 0; i < ln; i++) {
703 item = markerGroup.getAt(count++);
705 rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store);
706 item.setAttributes(Ext.apply(endMarkerStyle || {}, rendererAttributes || {}), true);
710 for(; count < markerCount; count++) {
711 item = markerGroup.getAt(count);
717 if (chart.markerIndex) {
718 path.splice(1, 0, path[1], path[2]);
719 me.previousPath = path;
725 // @private called when a label is to be created.
726 onCreateLabel: function(storeItem, item, i, display) {
728 group = me.labelsGroup,
731 endLabelStyle = Ext.apply(config, me.seriesLabelStyle);
733 return me.chart.surface.add(Ext.apply({
735 'text-anchor': 'middle',
738 'y': bbox.y + bbox.height / 2
739 }, endLabelStyle || {}));
742 // @private called when a label is to be created.
743 onPlaceLabel: function(label, storeItem, item, i, display, animate) {
746 resizing = chart.resizing,
748 format = config.renderer,
749 field = config.field,
753 radius = item.sprite.attr.radius,
756 label.setAttributes({
757 text: format(storeItem.get(field)),
761 if (display == 'rotate') {
762 label.setAttributes({
763 'text-anchor': 'start',
770 //correct label position to fit into the box
771 bb = label.getBBox();
774 x = x < bbox.x? bbox.x : x;
775 x = (x + width > bbox.x + bbox.width)? (x - (x + width - bbox.x - bbox.width)) : x;
776 y = (y - height < bbox.y)? bbox.y + height : y;
778 } else if (display == 'under' || display == 'over') {
779 //TODO(nicolas): find out why width/height values in circle bounding boxes are undefined.
780 bb = item.sprite.getBBox();
781 bb.width = bb.width || (radius * 2);
782 bb.height = bb.height || (radius * 2);
783 y = y + (display == 'over'? -bb.height : bb.height);
784 //correct label position to fit into the box
785 bb = label.getBBox();
787 height = bb.height/2;
788 x = x - width < bbox.x? bbox.x + width : x;
789 x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
790 y = y - height < bbox.y? bbox.y + height : y;
791 y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
794 if (me.chart.animate && !me.chart.resizing) {
796 me.onAnimate(label, {
803 label.setAttributes({
808 me.animation.on('afteranimate', function() {
817 //@private Overriding highlights.js highlightItem method.
818 highlightItem: function() {
820 me.callParent(arguments);
821 if (this.line && !this.highlighted) {
822 if (!('__strokeWidth' in this.line)) {
823 this.line.__strokeWidth = this.line.attr['stroke-width'] || 0;
825 if (this.line.__anim) {
826 this.line.__anim.paused = true;
828 this.line.__anim = Ext.create('Ext.fx.Anim', {
831 'stroke-width': this.line.__strokeWidth + 3
834 this.highlighted = true;
838 //@private Overriding highlights.js unHighlightItem method.
839 unHighlightItem: function() {
841 me.callParent(arguments);
842 if (this.line && this.highlighted) {
843 this.line.__anim = Ext.create('Ext.fx.Anim', {
846 'stroke-width': this.line.__strokeWidth
849 this.highlighted = false;
853 //@private called when a callout needs to be placed.
854 onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) {
861 surface = chart.surface,
862 resizing = chart.resizing,
863 config = me.callouts,
865 prev = i == 0? false : items[i -1].point,
866 next = (i == items.length -1)? false : items[i +1].point,
867 cur = [+item.point[0], +item.point[1]],
868 dir, norm, normal, a, aprev, anext,
869 offsetFromViz = config.offsetFromViz || 30,
870 offsetToSide = config.offsetToSide || 10,
871 offsetBox = config.offsetBox || 3,
872 boxx, boxy, boxw, boxh,
873 p, clipRect = me.clipRect,
875 width: config.styles.width || 10,
876 height: config.styles.height || 10
880 //get the right two points
887 a = (next[1] - prev[1]) / (next[0] - prev[0]);
888 aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]);
889 anext = (next[1] - cur[1]) / (next[0] - cur[0]);
891 norm = Math.sqrt(1 + a * a);
892 dir = [1 / norm, a / norm];
893 normal = [-dir[1], dir[0]];
895 //keep the label always on the outer part of the "elbow"
896 if (aprev > 0 && anext < 0 && normal[1] < 0
897 || aprev < 0 && anext > 0 && normal[1] > 0) {
900 } else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0
901 || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) {
906 x = cur[0] + normal[0] * offsetFromViz;
907 y = cur[1] + normal[1] * offsetFromViz;
909 //box position and dimensions
910 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
911 boxy = y - bbox.height /2 - offsetBox;
912 boxw = bbox.width + 2 * offsetBox;
913 boxh = bbox.height + 2 * offsetBox;
915 //now check if we're out of bounds and invert the normal vector correspondingly
916 //this may add new overlaps between labels (but labels won't be out of bounds).
917 if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) {
920 if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) {
925 x = cur[0] + normal[0] * offsetFromViz;
926 y = cur[1] + normal[1] * offsetFromViz;
928 //update box position and dimensions
929 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
930 boxy = y - bbox.height /2 - offsetBox;
931 boxw = bbox.width + 2 * offsetBox;
932 boxh = bbox.height + 2 * offsetBox;
935 //set the line from the middle of the pie to the box.
936 me.onAnimate(callout.lines, {
938 path: ["M", cur[0], cur[1], "L", x, y, "Z"]
941 //set component position
943 callout.panel.setPosition(boxx, boxy, true);
947 //set the line from the middle of the pie to the box.
948 callout.lines.setAttributes({
949 path: ["M", cur[0], cur[1], "L", x, y, "Z"]
951 //set component position
953 callout.panel.setPosition(boxx, boxy);
957 callout[p].show(true);
961 isItemInPoint: function(x, y, item, i) {
964 tolerance = me.selectionTolerance,
977 dist1, dist2, dist, midx, midy,
978 sqrt = Math.sqrt, abs = Math.abs;
981 prevItem = i && items[i - 1];
984 prevItem = items[ln - 1];
986 prevPoint = prevItem && prevItem.point;
987 nextPoint = nextItem && nextItem.point;
988 x1 = prevItem ? prevPoint[0] : nextPoint[0] - tolerance;
989 y1 = prevItem ? prevPoint[1] : nextPoint[1];
990 x2 = nextItem ? nextPoint[0] : prevPoint[0] + tolerance;
991 y2 = nextItem ? nextPoint[1] : prevPoint[1];
992 dist1 = sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1));
993 dist2 = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2));
994 dist = Math.min(dist1, dist2);
996 if (dist <= tolerance) {
997 return dist == dist1? prevItem : nextItem;
1002 // @private toggle visibility of all series elements (markers, sprites).
1003 toggleAll: function(show) {
1005 i, ln, shadow, shadows;
1007 Ext.chart.series.Line.superclass.hideAll.call(me);
1010 Ext.chart.series.Line.superclass.showAll.call(me);
1013 me.line.setAttributes({
1017 if (me.line.shadows) {
1018 for (i = 0, shadows = me.line.shadows, ln = shadows.length; i < ln; i++) {
1019 shadow = shadows[i];
1020 shadow.setAttributes({
1027 me.fillPath.setAttributes({
1033 // @private hide all series elements (markers, sprites).
1034 hideAll: function() {
1035 this.toggleAll(false);
1038 // @private hide all series elements (markers, sprites).
1039 showAll: function() {
1040 this.toggleAll(true);