Chart.Line.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. (function(){
  2. "use strict";
  3. var root = this,
  4. Chart = root.Chart,
  5. helpers = Chart.helpers;
  6. var defaultConfig = {
  7. ///Boolean - Whether grid lines are shown across the chart
  8. scaleShowGridLines : true,
  9. //String - Colour of the grid lines
  10. scaleGridLineColor : "rgba(0,0,0,.05)",
  11. //Number - Width of the grid lines
  12. scaleGridLineWidth : 1,
  13. //Boolean - Whether to show horizontal lines (except X axis)
  14. scaleShowHorizontalLines: true,
  15. //Boolean - Whether to show vertical lines (except Y axis)
  16. scaleShowVerticalLines: true,
  17. //Boolean - Whether the line is curved between points
  18. bezierCurve : true,
  19. //Number - Tension of the bezier curve between points
  20. bezierCurveTension : 0.4,
  21. //Boolean - Whether to show a dot for each point
  22. pointDot : true,
  23. //Number - Radius of each point dot in pixels
  24. pointDotRadius : 4,
  25. //Number - Pixel width of point dot stroke
  26. pointDotStrokeWidth : 1,
  27. //Number - amount extra to add to the radius to cater for hit detection outside the drawn point
  28. pointHitDetectionRadius : 20,
  29. //Boolean - Whether to show a stroke for datasets
  30. datasetStroke : true,
  31. //Number - Pixel width of dataset stroke
  32. datasetStrokeWidth : 2,
  33. //Boolean - Whether to fill the dataset with a colour
  34. datasetFill : true,
  35. //String - A legend template
  36. legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
  37. };
  38. Chart.Type.extend({
  39. name: "Line",
  40. defaults : defaultConfig,
  41. initialize: function(data){
  42. //Declare the extension of the default point, to cater for the options passed in to the constructor
  43. this.PointClass = Chart.Point.extend({
  44. strokeWidth : this.options.pointDotStrokeWidth,
  45. radius : this.options.pointDotRadius,
  46. display: this.options.pointDot,
  47. hitDetectionRadius : this.options.pointHitDetectionRadius,
  48. ctx : this.chart.ctx,
  49. inRange : function(mouseX){
  50. return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2));
  51. }
  52. });
  53. this.datasets = [];
  54. //Set up tooltip events on the chart
  55. if (this.options.showTooltips){
  56. helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
  57. var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : [];
  58. this.eachPoints(function(point){
  59. point.restore(['fillColor', 'strokeColor']);
  60. });
  61. helpers.each(activePoints, function(activePoint){
  62. activePoint.fillColor = activePoint.highlightFill;
  63. activePoint.strokeColor = activePoint.highlightStroke;
  64. });
  65. this.showTooltip(activePoints);
  66. });
  67. }
  68. //Iterate through each of the datasets, and build this into a property of the chart
  69. helpers.each(data.datasets,function(dataset){
  70. var datasetObject = {
  71. label : dataset.label || null,
  72. fillColor : dataset.fillColor,
  73. strokeColor : dataset.strokeColor,
  74. pointColor : dataset.pointColor,
  75. pointStrokeColor : dataset.pointStrokeColor,
  76. points : []
  77. };
  78. this.datasets.push(datasetObject);
  79. helpers.each(dataset.data,function(dataPoint,index){
  80. //Add a new point for each piece of data, passing any required data to draw.
  81. datasetObject.points.push(new this.PointClass({
  82. value : dataPoint,
  83. label : data.labels[index],
  84. datasetLabel: dataset.label,
  85. strokeColor : dataset.pointStrokeColor,
  86. fillColor : dataset.pointColor,
  87. highlightFill : dataset.pointHighlightFill || dataset.pointColor,
  88. highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor
  89. }));
  90. },this);
  91. this.buildScale(data.labels);
  92. this.eachPoints(function(point, index){
  93. helpers.extend(point, {
  94. x: this.scale.calculateX(index),
  95. y: this.scale.endPoint
  96. });
  97. point.save();
  98. }, this);
  99. },this);
  100. this.render();
  101. },
  102. update : function(){
  103. this.scale.update();
  104. // Reset any highlight colours before updating.
  105. helpers.each(this.activeElements, function(activeElement){
  106. activeElement.restore(['fillColor', 'strokeColor']);
  107. });
  108. this.eachPoints(function(point){
  109. point.save();
  110. });
  111. this.render();
  112. },
  113. eachPoints : function(callback){
  114. helpers.each(this.datasets,function(dataset){
  115. helpers.each(dataset.points,callback,this);
  116. },this);
  117. },
  118. getPointsAtEvent : function(e){
  119. var pointsArray = [],
  120. eventPosition = helpers.getRelativePosition(e);
  121. helpers.each(this.datasets,function(dataset){
  122. helpers.each(dataset.points,function(point){
  123. if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point);
  124. });
  125. },this);
  126. return pointsArray;
  127. },
  128. buildScale : function(labels){
  129. var self = this;
  130. var dataTotal = function(){
  131. var values = [];
  132. self.eachPoints(function(point){
  133. values.push(point.value);
  134. });
  135. return values;
  136. };
  137. var scaleOptions = {
  138. templateString : this.options.scaleLabel,
  139. height : this.chart.height,
  140. width : this.chart.width,
  141. ctx : this.chart.ctx,
  142. textColor : this.options.scaleFontColor,
  143. fontSize : this.options.scaleFontSize,
  144. fontStyle : this.options.scaleFontStyle,
  145. fontFamily : this.options.scaleFontFamily,
  146. valuesCount : labels.length,
  147. beginAtZero : this.options.scaleBeginAtZero,
  148. integersOnly : this.options.scaleIntegersOnly,
  149. calculateYRange : function(currentHeight){
  150. var updatedRanges = helpers.calculateScaleRange(
  151. dataTotal(),
  152. currentHeight,
  153. this.fontSize,
  154. this.beginAtZero,
  155. this.integersOnly
  156. );
  157. helpers.extend(this, updatedRanges);
  158. },
  159. xLabels : labels,
  160. font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily),
  161. lineWidth : this.options.scaleLineWidth,
  162. lineColor : this.options.scaleLineColor,
  163. showHorizontalLines : this.options.scaleShowHorizontalLines,
  164. showVerticalLines : this.options.scaleShowVerticalLines,
  165. gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
  166. gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)",
  167. padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth,
  168. showLabels : this.options.scaleShowLabels,
  169. display : this.options.showScale
  170. };
  171. if (this.options.scaleOverride){
  172. helpers.extend(scaleOptions, {
  173. calculateYRange: helpers.noop,
  174. steps: this.options.scaleSteps,
  175. stepValue: this.options.scaleStepWidth,
  176. min: this.options.scaleStartValue,
  177. max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
  178. });
  179. }
  180. this.scale = new Chart.Scale(scaleOptions);
  181. },
  182. addData : function(valuesArray,label){
  183. //Map the values array for each of the datasets
  184. helpers.each(valuesArray,function(value,datasetIndex){
  185. //Add a new point for each piece of data, passing any required data to draw.
  186. this.datasets[datasetIndex].points.push(new this.PointClass({
  187. value : value,
  188. label : label,
  189. x: this.scale.calculateX(this.scale.valuesCount+1),
  190. y: this.scale.endPoint,
  191. strokeColor : this.datasets[datasetIndex].pointStrokeColor,
  192. fillColor : this.datasets[datasetIndex].pointColor
  193. }));
  194. },this);
  195. this.scale.addXLabel(label);
  196. //Then re-render the chart.
  197. this.update();
  198. },
  199. removeData : function(){
  200. this.scale.removeXLabel();
  201. //Then re-render the chart.
  202. helpers.each(this.datasets,function(dataset){
  203. dataset.points.shift();
  204. },this);
  205. this.update();
  206. },
  207. reflow : function(){
  208. var newScaleProps = helpers.extend({
  209. height : this.chart.height,
  210. width : this.chart.width
  211. });
  212. this.scale.update(newScaleProps);
  213. },
  214. draw : function(ease){
  215. var easingDecimal = ease || 1;
  216. this.clear();
  217. var ctx = this.chart.ctx;
  218. // Some helper methods for getting the next/prev points
  219. var hasValue = function(item){
  220. return item.value !== null;
  221. },
  222. nextPoint = function(point, collection, index){
  223. return helpers.findNextWhere(collection, hasValue, index) || point;
  224. },
  225. previousPoint = function(point, collection, index){
  226. return helpers.findPreviousWhere(collection, hasValue, index) || point;
  227. };
  228. this.scale.draw(easingDecimal);
  229. helpers.each(this.datasets,function(dataset){
  230. var pointsWithValues = helpers.where(dataset.points, hasValue);
  231. //Transition each point first so that the line and point drawing isn't out of sync
  232. //We can use this extra loop to calculate the control points of this dataset also in this loop
  233. helpers.each(dataset.points, function(point, index){
  234. if (point.hasValue()){
  235. point.transition({
  236. y : this.scale.calculateY(point.value),
  237. x : this.scale.calculateX(index)
  238. }, easingDecimal);
  239. }
  240. },this);
  241. // Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point
  242. // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed
  243. if (this.options.bezierCurve){
  244. helpers.each(pointsWithValues, function(point, index){
  245. var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0;
  246. point.controlPoints = helpers.splineCurve(
  247. previousPoint(point, pointsWithValues, index),
  248. point,
  249. nextPoint(point, pointsWithValues, index),
  250. tension
  251. );
  252. // Prevent the bezier going outside of the bounds of the graph
  253. // Cap puter bezier handles to the upper/lower scale bounds
  254. if (point.controlPoints.outer.y > this.scale.endPoint){
  255. point.controlPoints.outer.y = this.scale.endPoint;
  256. }
  257. else if (point.controlPoints.outer.y < this.scale.startPoint){
  258. point.controlPoints.outer.y = this.scale.startPoint;
  259. }
  260. // Cap inner bezier handles to the upper/lower scale bounds
  261. if (point.controlPoints.inner.y > this.scale.endPoint){
  262. point.controlPoints.inner.y = this.scale.endPoint;
  263. }
  264. else if (point.controlPoints.inner.y < this.scale.startPoint){
  265. point.controlPoints.inner.y = this.scale.startPoint;
  266. }
  267. },this);
  268. }
  269. //Draw the line between all the points
  270. ctx.lineWidth = this.options.datasetStrokeWidth;
  271. ctx.strokeStyle = dataset.strokeColor;
  272. ctx.beginPath();
  273. helpers.each(pointsWithValues, function(point, index){
  274. if (index === 0){
  275. ctx.moveTo(point.x, point.y);
  276. }
  277. else{
  278. if(this.options.bezierCurve){
  279. var previous = previousPoint(point, pointsWithValues, index);
  280. ctx.bezierCurveTo(
  281. previous.controlPoints.outer.x,
  282. previous.controlPoints.outer.y,
  283. point.controlPoints.inner.x,
  284. point.controlPoints.inner.y,
  285. point.x,
  286. point.y
  287. );
  288. }
  289. else{
  290. ctx.lineTo(point.x,point.y);
  291. }
  292. }
  293. }, this);
  294. ctx.stroke();
  295. if (this.options.datasetFill && pointsWithValues.length > 0){
  296. //Round off the line by going to the base of the chart, back to the start, then fill.
  297. ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint);
  298. ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint);
  299. ctx.fillStyle = dataset.fillColor;
  300. ctx.closePath();
  301. ctx.fill();
  302. }
  303. //Now draw the points over the line
  304. //A little inefficient double looping, but better than the line
  305. //lagging behind the point positions
  306. helpers.each(pointsWithValues,function(point){
  307. point.draw();
  308. });
  309. },this);
  310. }
  311. });
  312. }).call(this);