1 <!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'>/**
2 </span> * @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` property of the store and the other to `data3`. The type for both configurations is
80 * `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
81 * 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
82 * when hovered. The second series has `fill=true` which means that the line will also have an area below it of the same color.
85 Ext.define('Ext.chart.series.Line', {
87 /* Begin Definitions */
89 extend: 'Ext.chart.series.Cartesian',
91 alternateClassName: ['Ext.chart.LineSeries', 'Ext.chart.LineChart'],
93 requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shape', 'Ext.draw.Draw', 'Ext.fx.Anim'],
101 <span id='Ext-chart.series.Line-cfg-selectionTolerance'> /**
102 </span> * @cfg {Number} selectionTolerance
103 * The offset distance from the cursor position to the line series to trigger events (then used for highlighting series, etc).
105 selectionTolerance: 20,
107 <span id='Ext-chart.series.Line-cfg-showMarkers'> /**
108 </span> * @cfg {Boolean} showMarkers
109 * Whether markers should be displayed at the data points along the line. If true,
110 * then the {@link #markerConfig} config item will determine the markers' styling.
114 <span id='Ext-chart.series.Line-cfg-markerConfig'> /**
115 </span> * @cfg {Object} markerConfig
116 * The display style for the markers. Only used if {@link #showMarkers} is true.
117 * The markerConfig is a configuration object containing the same set of properties defined in
118 * the Sprite class. For example, if we were to set red circles as markers to the line series we could
121 <pre><code>
127 </code></pre>
132 <span id='Ext-chart.series.Line-cfg-style'> /**
133 </span> * @cfg {Object} style
134 * An object containing styles for the visualization lines. These styles will override the theme styles.
135 * Some options contained within the style object will are described next.
139 <span id='Ext-chart.series.Line-cfg-smooth'> /**
140 </span> * @cfg {Boolean} smooth
141 * If true, the line will be smoothed/rounded around its points, otherwise straight line
142 * segments will be drawn. Defaults to false.
146 <span id='Ext-chart.series.Line-cfg-fill'> /**
147 </span> * @cfg {Boolean} fill
148 * If true, the area below the line will be filled in using the {@link #style.eefill} and
149 * {@link #style.opacity} config properties. Defaults to false.
153 constructor: function(config) {
154 this.callParent(arguments);
156 surface = me.chart.surface,
157 shadow = me.chart.shadow,
159 Ext.apply(me, config, {
164 "stroke-width": 6,
165 "stroke-opacity": 0.05,
166 stroke: 'rgb(0, 0, 0)',
172 "stroke-width": 4,
173 "stroke-opacity": 0.1,
174 stroke: 'rgb(0, 0, 0)',
180 "stroke-width": 2,
181 "stroke-opacity": 0.15,
182 stroke: 'rgb(0, 0, 0)',
189 me.group = surface.getGroup(me.seriesId);
190 if (me.showMarkers) {
191 me.markerGroup = surface.getGroup(me.seriesId + '-markers');
194 for (i = 0, l = this.shadowAttributes.length; i < l; i++) {
195 me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
200 // @private makes an average of points when there are more data points than pixels to be rendered.
201 shrink: function(xValues, yValues, size) {
202 // Start at the 2nd point...
203 var len = xValues.length,
204 ratio = Math.floor(len / size),
211 for (; i < len; ++i) {
212 xSum += xValues[i] || 0;
213 ySum += yValues[i] || 0;
214 if (i % ratio == 0) {
215 xRes.push(xSum/ratio);
216 yRes.push(ySum/ratio);
227 <span id='Ext-chart.series.Line-method-drawSeries'> /**
228 </span> * Draws the series for the current chart.
230 drawSeries: function() {
233 store = chart.substore || chart.store,
234 surface = chart.surface,
235 chartBBox = chart.chartBBox,
238 gutterX = chart.maxGutter[0],
239 gutterY = chart.maxGutter[1],
240 showMarkers = me.showMarkers,
241 markerGroup = me.markerGroup,
242 enableShadows = chart.shadow,
243 shadowGroups = me.shadowGroups,
244 shadowAttributes = this.shadowAttributes,
245 lnsh = shadowGroups.length,
246 dummyPath = ["M"],
247 path = ["M"],
248 markerIndex = chart.markerIndex,
249 axes = [].concat(me.axis),
255 markerStyle = me.markerStyle,
256 seriesStyle = me.seriesStyle,
257 seriesLabelStyle = me.seriesLabelStyle,
258 colorArrayStyle = me.colorArrayStyle,
259 colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
260 seriesIdx = me.seriesIdx, shadows, shadow, shindex, fromPath, fill, fillPath, rendererAttributes,
261 x, y, prevX, prevY, firstY, markerCount, i, j, ln, axis, ends, marker, markerAux, item, xValue,
262 yValue, coords, xScale, yScale, minX, maxX, minY, maxY, line, animation, endMarkerStyle,
263 endLineStyle, type, props, firstMarker;
265 //if store is empty then there's nothing to draw.
266 if (!store || !store.getCount()) {
270 //prepare style objects for line and markers
271 endMarkerStyle = Ext.apply(markerStyle, me.markerConfig);
272 type = endMarkerStyle.type;
273 delete endMarkerStyle.type;
274 endLineStyle = Ext.apply(seriesStyle, me.style);
275 //if no stroke with is specified force it to 0.5 because this is
276 //about making *lines*
277 if (!endLineStyle['stroke-width']) {
278 endLineStyle['stroke-width'] = 0.5;
280 //If we're using a time axis and we need to translate the points,
281 //then reuse the first markers as the last markers.
282 if (markerIndex && markerGroup && markerGroup.getCount()) {
283 for (i = 0; i < markerIndex; i++) {
284 marker = markerGroup.getAt(i);
285 markerGroup.remove(marker);
286 markerGroup.add(marker);
287 markerAux = markerGroup.getAt(markerGroup.getCount() - 2);
288 marker.setAttributes({
292 x: markerAux.attr.translation.x,
293 y: markerAux.attr.translation.y
299 me.unHighlightItem();
300 me.cleanHighlights();
305 me.clipRect = [bbox.x, bbox.y, bbox.width, bbox.height];
307 for (i = 0, ln = axes.length; i < ln; i++) {
308 axis = chart.axes.get(axes[i]);
310 ends = axis.calcEnds();
311 if (axis.position == 'top' || axis.position == 'bottom') {
321 // If a field was specified without a corresponding axis, create one to get bounds
322 //only do this for the axis where real values are bound (that's why we check for
324 if (me.xField && !Ext.isNumber(minX)
325 && (me.axis == 'bottom' || me.axis == 'top')) {
326 axis = Ext.create('Ext.chart.axis.Axis', {
328 fields: [].concat(me.xField)
333 if (me.yField && !Ext.isNumber(minY)
334 && (me.axis == 'right' || me.axis == 'left')) {
335 axis = Ext.create('Ext.chart.axis.Axis', {
337 fields: [].concat(me.yField)
345 xScale = bbox.width / (store.getCount() - 1);
348 xScale = bbox.width / (maxX - minX);
353 yScale = bbox.height / (store.getCount() - 1);
356 yScale = bbox.height / (maxY - minY);
359 store.each(function(record, i) {
360 xValue = record.get(me.xField);
361 yValue = record.get(me.yField);
362 //skip undefined values
363 if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) {
365 if (Ext.isDefined(Ext.global.console)) {
366 Ext.global.console.warn("[Ext.chart.series.Line] Skipping a store element with an undefined value at ", record, xValue, yValue);
372 if (typeof xValue == 'string' || typeof xValue == 'object'
373 //set as uniform distribution if the axis is a category axis.
374 || (me.axis != 'top' && me.axis != 'bottom')) {
377 if (typeof yValue == 'string' || typeof yValue == 'object'
378 //set as uniform distribution if the axis is a category axis.
379 || (me.axis != 'left' && me.axis != 'right')) {
382 xValues.push(xValue);
383 yValues.push(yValue);
387 if (ln > bbox.width) {
388 coords = me.shrink(xValues, yValues, bbox.width);
396 for (i = 0; i < ln; i++) {
399 if (yValue === false) {
400 if (path.length == 1) {
404 me.items.push(false);
407 x = (bbox.x + (xValue - minX) * xScale).toFixed(2);
408 y = ((bbox.y + bbox.height) - (yValue - minY) * yScale).toFixed(2);
413 path = path.concat([x, y]);
415 if ((typeof firstY == 'undefined') && (typeof y != 'undefined')) {
418 // If this is the first line, create a dummypath to animate in from.
419 if (!me.line || chart.resizing) {
420 dummyPath = dummyPath.concat([x, bbox.y + bbox.height / 2]);
423 // When resizing, reset before animating
424 if (chart.animate && chart.resizing && me.line) {
425 me.line.setAttributes({
429 me.fillPath.setAttributes({
434 if (me.line.shadows) {
435 shadows = me.line.shadows;
436 for (j = 0, lnsh = shadows.length; j < lnsh; j++) {
438 shadow.setAttributes({
445 marker = markerGroup.getAt(i);
447 marker = Ext.chart.Shape[type](surface, Ext.apply({
448 group: [group, markerGroup],
452 y: prevY || (bbox.y + bbox.height / 2)
454 value: '"' + xValue + ', ' + yValue + '"'
463 marker.setAttributes({
464 value: '"' + xValue + ', ' + yValue + '"',
477 value: [xValue, yValue],
480 storeItem: store.getAt(i)
486 if (path.length <= 1) {
487 //nothing to be rendered
492 path = Ext.draw.Draw.smooth(path, 6);
495 //Correct path if we're animating timeAxis intervals
496 if (chart.markerIndex && me.previousPath) {
497 fromPath = me.previousPath;
498 fromPath.splice(1, 2);
503 // Only create a line if one doesn't exist.
505 me.line = surface.add(Ext.apply({
509 stroke: endLineStyle.stroke || endLineStyle.fill
510 }, endLineStyle || {}));
511 //unset fill here (there's always a default fill withing the themes).
512 me.line.setAttributes({
515 if (!endLineStyle.stroke && colorArrayLength) {
516 me.line.setAttributes({
517 stroke: colorArrayStyle[seriesIdx % colorArrayLength]
522 shadows = me.line.shadows = [];
523 for (shindex = 0; shindex < lnsh; shindex++) {
524 shadowBarAttr = shadowAttributes[shindex];
525 shadowBarAttr = Ext.apply({}, shadowBarAttr, { path: dummyPath });
526 shadow = chart.surface.add(Ext.apply({}, {
528 group: shadowGroups[shindex]
530 shadows.push(shadow);
535 fillPath = path.concat([
536 ["L", x, bbox.y + bbox.height],
537 ["L", bbox.x, bbox.y + bbox.height],
538 ["L", bbox.x, firstY]
541 me.fillPath = surface.add({
544 opacity: endLineStyle.opacity || 0.3,
545 fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill,
550 markerCount = showMarkers && markerGroup.getCount();
554 //Add renderer to line. There is not unique record associated with this.
555 rendererAttributes = me.renderer(line, false, { path: path }, i, store);
556 Ext.apply(rendererAttributes, endLineStyle || {}, {
557 stroke: endLineStyle.stroke || endLineStyle.fill
559 //fill should not be used here but when drawing the special fill path object
560 delete rendererAttributes.fill;
561 if (chart.markerIndex && me.previousPath) {
562 me.animation = animation = me.onAnimate(line, {
563 to: rendererAttributes,
569 me.animation = animation = me.onAnimate(line, {
570 to: rendererAttributes
575 shadows = line.shadows;
576 for(j = 0; j < lnsh; j++) {
577 if (chart.markerIndex && me.previousPath) {
578 me.onAnimate(shadows[j], {
580 from: { path: fromPath }
583 me.onAnimate(shadows[j], {
591 me.onAnimate(me.fillPath, {
594 fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill
595 }, endLineStyle || {})
600 for(i = 0; i < ln; i++) {
601 item = markerGroup.getAt(i);
604 rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store);
606 to: Ext.apply(rendererAttributes, endMarkerStyle || {})
609 item.setAttributes(Ext.apply({
615 for(; i < markerCount; i++) {
616 item = markerGroup.getAt(i);
619 // for(i = 0; i < (chart.markerIndex || 0)-1; i++) {
620 // item = markerGroup.getAt(i);
625 rendererAttributes = me.renderer(me.line, false, { path: path, hidden: false }, i, store);
626 Ext.apply(rendererAttributes, endLineStyle || {}, {
627 stroke: endLineStyle.stroke || endLineStyle.fill
629 //fill should not be used here but when drawing the special fill path object
630 delete rendererAttributes.fill;
631 me.line.setAttributes(rendererAttributes, true);
632 //set path for shadows
634 shadows = me.line.shadows;
635 for(j = 0; j < lnsh; j++) {
636 shadows[j].setAttributes({
642 me.fillPath.setAttributes({
647 for(i = 0; i < ln; i++) {
648 item = markerGroup.getAt(i);
651 rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store);
652 item.setAttributes(Ext.apply(endMarkerStyle || {}, rendererAttributes || {}), true);
658 for(; i < markerCount; i++) {
659 item = markerGroup.getAt(i);
665 if (chart.markerIndex) {
666 path.splice(1, 0, path[1], path[2]);
667 me.previousPath = path;
673 // @private called when a label is to be created.
674 onCreateLabel: function(storeItem, item, i, display) {
676 group = me.labelsGroup,
679 endLabelStyle = Ext.apply(config, me.seriesLabelStyle);
681 return me.chart.surface.add(Ext.apply({
683 'text-anchor': 'middle',
686 'y': bbox.y + bbox.height / 2
687 }, endLabelStyle || {}));
690 // @private called when a label is to be created.
691 onPlaceLabel: function(label, storeItem, item, i, display, animate) {
694 resizing = chart.resizing,
696 format = config.renderer,
697 field = config.field,
701 radius = item.sprite.attr.radius,
704 label.setAttributes({
705 text: format(storeItem.get(field)),
709 if (display == 'rotate') {
710 label.setAttributes({
711 'text-anchor': 'start',
718 //correct label position to fit into the box
719 bb = label.getBBox();
722 x = x < bbox.x? bbox.x : x;
723 x = (x + width > bbox.x + bbox.width)? (x - (x + width - bbox.x - bbox.width)) : x;
724 y = (y - height < bbox.y)? bbox.y + height : y;
726 } else if (display == 'under' || display == 'over') {
727 //TODO(nicolas): find out why width/height values in circle bounding boxes are undefined.
728 bb = item.sprite.getBBox();
729 bb.width = bb.width || (radius * 2);
730 bb.height = bb.height || (radius * 2);
731 y = y + (display == 'over'? -bb.height : bb.height);
732 //correct label position to fit into the box
733 bb = label.getBBox();
735 height = bb.height/2;
736 x = x - width < bbox.x? bbox.x + width : x;
737 x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
738 y = y - height < bbox.y? bbox.y + height : y;
739 y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
742 if (me.chart.animate && !me.chart.resizing) {
744 me.onAnimate(label, {
751 label.setAttributes({
756 me.animation.on('afteranimate', function() {
765 //@private Overriding highlights.js highlightItem method.
766 highlightItem: function() {
768 me.callParent(arguments);
769 if (this.line && !this.highlighted) {
770 if (!('__strokeWidth' in this.line)) {
771 this.line.__strokeWidth = this.line.attr['stroke-width'] || 0;
773 if (this.line.__anim) {
774 this.line.__anim.paused = true;
776 this.line.__anim = Ext.create('Ext.fx.Anim', {
779 'stroke-width': this.line.__strokeWidth + 3
782 this.highlighted = true;
786 //@private Overriding highlights.js unHighlightItem method.
787 unHighlightItem: function() {
789 me.callParent(arguments);
790 if (this.line && this.highlighted) {
791 this.line.__anim = Ext.create('Ext.fx.Anim', {
794 'stroke-width': this.line.__strokeWidth
797 this.highlighted = false;
801 //@private called when a callout needs to be placed.
802 onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) {
809 surface = chart.surface,
810 resizing = chart.resizing,
811 config = me.callouts,
813 prev = i == 0? false : items[i -1].point,
814 next = (i == items.length -1)? false : items[i +1].point,
815 cur = [+item.point[0], +item.point[1]],
816 dir, norm, normal, a, aprev, anext,
817 offsetFromViz = config.offsetFromViz || 30,
818 offsetToSide = config.offsetToSide || 10,
819 offsetBox = config.offsetBox || 3,
820 boxx, boxy, boxw, boxh,
821 p, clipRect = me.clipRect,
823 width: config.styles.width || 10,
824 height: config.styles.height || 10
828 //get the right two points
835 a = (next[1] - prev[1]) / (next[0] - prev[0]);
836 aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]);
837 anext = (next[1] - cur[1]) / (next[0] - cur[0]);
839 norm = Math.sqrt(1 + a * a);
840 dir = [1 / norm, a / norm];
841 normal = [-dir[1], dir[0]];
843 //keep the label always on the outer part of the "elbow"
844 if (aprev > 0 && anext < 0 && normal[1] < 0
845 || aprev < 0 && anext > 0 && normal[1] > 0) {
848 } else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0
849 || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) {
854 x = cur[0] + normal[0] * offsetFromViz;
855 y = cur[1] + normal[1] * offsetFromViz;
857 //box position and dimensions
858 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
859 boxy = y - bbox.height /2 - offsetBox;
860 boxw = bbox.width + 2 * offsetBox;
861 boxh = bbox.height + 2 * offsetBox;
863 //now check if we're out of bounds and invert the normal vector correspondingly
864 //this may add new overlaps between labels (but labels won't be out of bounds).
865 if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) {
868 if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) {
873 x = cur[0] + normal[0] * offsetFromViz;
874 y = cur[1] + normal[1] * offsetFromViz;
876 //update box position and dimensions
877 boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
878 boxy = y - bbox.height /2 - offsetBox;
879 boxw = bbox.width + 2 * offsetBox;
880 boxh = bbox.height + 2 * offsetBox;
883 //set the line from the middle of the pie to the box.
884 me.onAnimate(callout.lines, {
886 path: ["M", cur[0], cur[1], "L", x, y, "Z"]
889 //set component position
891 callout.panel.setPosition(boxx, boxy, true);
895 //set the line from the middle of the pie to the box.
896 callout.lines.setAttributes({
897 path: ["M", cur[0], cur[1], "L", x, y, "Z"]
899 //set component position
901 callout.panel.setPosition(boxx, boxy);
905 callout[p].show(true);
909 isItemInPoint: function(x, y, item, i) {
912 tolerance = me.selectionTolerance,
925 dist1, dist2, dist, midx, midy,
926 sqrt = Math.sqrt, abs = Math.abs;
929 prevItem = i && items[i - 1];
932 prevItem = items[ln - 1];
934 prevPoint = prevItem && prevItem.point;
935 nextPoint = nextItem && nextItem.point;
936 x1 = prevItem ? prevPoint[0] : nextPoint[0] - tolerance;
937 y1 = prevItem ? prevPoint[1] : nextPoint[1];
938 x2 = nextItem ? nextPoint[0] : prevPoint[0] + tolerance;
939 y2 = nextItem ? nextPoint[1] : prevPoint[1];
940 dist1 = sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1));
941 dist2 = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2));
942 dist = Math.min(dist1, dist2);
944 if (dist <= tolerance) {
945 return dist == dist1? prevItem : nextItem;
950 // @private toggle visibility of all series elements (markers, sprites).
951 toggleAll: function(show) {
953 i, ln, shadow, shadows;
955 Ext.chart.series.Line.superclass.hideAll.call(me);
958 Ext.chart.series.Line.superclass.showAll.call(me);
961 me.line.setAttributes({
965 if (me.line.shadows) {
966 for (i = 0, shadows = me.line.shadows, ln = shadows.length; i < ln; i++) {
968 shadow.setAttributes({
975 me.fillPath.setAttributes({
981 // @private hide all series elements (markers, sprites).
982 hideAll: function() {
983 this.toggleAll(false);
986 // @private hide all series elements (markers, sprites).
987 showAll: function() {
988 this.toggleAll(true);
990 });</pre></pre></body></html>