chart-line.js 11 KB

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