(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['Chart'], factory); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. factory(require('Chart')); } else { // Browser globals (root is window) factory(root.Chart); } }(this, function (chartjs) { "use strict"; var helpers = chartjs.helpers, hlp = { formatDateValue: function (date, tFormat, dFormat, useUtc) { date = new Date(+date); var ms = useUtc ? date.getUTCMilliseconds() : date.getMilliseconds(); if (ms) { return ('000' + ms).slice(-3); } var hasTime = useUtc ? date.getUTCHours() + date.getUTCMinutes() + date.getUTCSeconds() : date.getHours() + date.getMinutes() + date.getSeconds(); if (hasTime) { return dateFormat(date, tFormat || "h:MM", useUtc); } else { return dateFormat(date, dFormat || "mmm d", useUtc); } }, getElementOrDefault: function (array, index, defaultValue) { return index >= 0 && index < array.length ? array[index] : defaultValue; }, applyRange: function (value, min, max) { return value > max ? max : value < min ? min : value; }, ScatterPoint: chartjs.Point.extend({ inRange: function (chartX, chartY) { var hitDetectionRange = this.hitDetectionRadius + this.radius * this.size; return ((Math.pow(chartX - this.x, 2) + Math.pow(chartY - this.y, 2)) < Math.pow(hitDetectionRange, 2)); }, draw: function () { if (this.display && this.size > 0) { var ctx = this.ctx; ctx.beginPath(); ctx.arc(this.x, this.y, this.size * this.radius, 0, Math.PI * 2); ctx.closePath(); ctx.strokeStyle = this.strokeColor; ctx.lineWidth = this.strokeWidth; ctx.fillStyle = this.fillColor; ctx.fill(); ctx.stroke(); } } }) }; var dateFormat = function () { var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, timezoneClip = /[^-+\dA-Z]/g, pad = function (val, len) { val = String(val); len = len || 2; while (val.length < len) val = "0" + val; return val; }, masks = { "default": "ddd mmm dd yyyy HH:MM:ss" }, i18n = { dayNames: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], monthNames: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ] }; // Regexes and supporting functions are cached through closure return function (date, mask, utc) { // You can't provide utc if you skip other args (use the "UTC:" mask prefix) if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { mask = date; date = undefined; } // Passing date through Date applies Date.parse, if necessary date = date ? new Date(date) : new Date; if (isNaN(date)) throw SyntaxError("invalid date"); mask = String(masks[mask] || mask || masks["default"]); // Allow setting the utc argument via the mask if (mask.slice(0, 4) == "UTC:") { mask = mask.slice(4); utc = true; } var _ = utc ? "getUTC" : "get", d = date[_ + "Date"](), D = date[_ + "Day"](), m = date[_ + "Month"](), y = date[_ + "FullYear"](), H = date[_ + "Hours"](), M = date[_ + "Minutes"](), s = date[_ + "Seconds"](), L = date[_ + "Milliseconds"](), o = utc ? 0 : date.getTimezoneOffset(), flags = { d: d, dd: pad(d), ddd: i18n.dayNames[D], dddd: i18n.dayNames[D + 7], m: m + 1, mm: pad(m + 1), mmm: i18n.monthNames[m], mmmm: i18n.monthNames[m + 12], yy: String(y).slice(2), yyyy: y, h: H % 12 || 12, hh: pad(H % 12 || 12), H: H, HH: pad(H), M: M, MM: pad(M), s: s, ss: pad(s), l: pad(L, 3), L: pad(L > 99 ? Math.round(L / 10) : L), t: H < 12 ? "a" : "p", tt: H < 12 ? "am" : "pm", T: H < 12 ? "A" : "P", TT: H < 12 ? "AM" : "PM", Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] }; return mask.replace(token, function ($0) { return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); }); }; }(); var defaultConfig = { // INHERIT // showScale: true, // Boolean - If we should show the scale at all // scaleLineColor: "rgba(0,0,0,.1)", // String - Colour of the scale line // scaleLineWidth: 1, // Number - Pixel width of the scale line // scaleShowLabels: true, // Boolean - Whether to show labels on the scale // scaleLabel: "<%=value%>", // Interpolated JS string - can access value scaleArgLabel: "<%=value%>", // Interpolated JS string - can access value scaleSizeLabel: "<%=value%>", // Interpolated JS string - can access value emptyDataMessage: "chart has no data", // String - Message for empty data // SCALE scaleShowGridLines: true, //Boolean - Whether grid lines are shown across the chart scaleGridLineWidth: 1, //Number - Width of the grid lines scaleGridLineColor: "rgba(0,0,0,.05)", //String - Colour of the grid lines scaleShowHorizontalLines: true, //Boolean - Whether to show horizontal lines (except X axis) scaleShowVerticalLines: true, //Boolean - Whether to show vertical lines (except Y axis) // DATE SCALE scaleType: "number", useUtc: true, scaleDateFormat: "mmm d", scaleTimeFormat: "h:MM", scaleDateTimeFormat: "mmm d, yyyy, hh:MM", // LINES datasetStroke: true, // Boolean - Whether to show a stroke for datasets datasetStrokeWidth: 2, // Number - Pixel width of dataset stroke datasetStrokeColor: '#007ACC', // String - Color of dataset stroke datasetPointStrokeColor: 'white', // String - Color of dataset stroke bezierCurve: true, // Boolean - Whether the line is curved between points bezierCurveTension: 0.4, // Number - Tension of the bezier curve between points // POINTS pointDot: true, // Boolean - Whether to show a dot for each point pointDotStrokeWidth: 1, // Number - Pixel width of point dot stroke pointDotRadius: 4, // Number - Radius of each point dot in pixels pointHitDetectionRadius: 4, // Number - amount extra to add to the radius to cater for hit detection outside the drawn point multiTooltipTemplate: "<%=argLabel%>; <%=valueLabel%>", tooltipTemplate: "<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%=argLabel%>; <%=valueLabel%>", legendTemplate: "" }; chartjs.ScatterNumberScale = chartjs.Element.extend({ initialize: function () { // this.dataRange - минимальные и максимальные значения данных // инициализируем настройки // рассчитываем вспомогательные параметры this.font = helpers.fontString(this.fontSize, this.fontStyle, this.fontFamily); this.padding = this.fontSize / 2; }, setDataRange: function (dataRange) { this.dataRange = dataRange; }, api: { calculateControlPoints: function (prev, current, next, range, tension) { var tensionBefore = !!prev ? tension : 0; var tensionAfter = !!next ? tension : 0; var innerCurrent = current; var innerPrev = prev ? prev : current; var innerNext = next ? next : current; var a = { xx: innerCurrent.arg - innerPrev.arg, yy: innerCurrent.value - innerPrev.value } var b = { xx: innerNext.arg - innerPrev.arg, yy: innerNext.value - innerPrev.value } var mul = a.xx * b.xx + a.yy * b.yy; var mod = Math.sqrt(b.xx * b.xx + b.yy * b.yy); var k = Math.min(Math.max(mul / (mod * mod), 0.3), 0.7); var result = { before: { x: innerCurrent.arg - b.xx * k * tensionBefore, y: innerCurrent.value - b.yy * k * tensionBefore }, after: { x: innerCurrent.arg + b.xx * (1 - k) * tensionAfter, y: innerCurrent.value + b.yy * (1 - k) * tensionAfter } }; // Cap inner bezier handles to the upper/lower scale bounds result.before.y = hlp.applyRange(result.before.y, range.ymin, range.ymax); result.after.y = hlp.applyRange(result.after.y, range.ymin, range.ymax); return result; }, generateLabels: function (templateString, numberOfSteps, graphMin, stepValue) { var labelsArray = new Array(numberOfSteps + 1), stepDecimalPlaces = helpers.getDecimalPlaces(stepValue); if (templateString) { helpers.each(labelsArray, function (val, index) { labelsArray[index] = helpers.template(templateString, { value: (graphMin + (stepValue * (index))).toFixed(stepDecimalPlaces) }); }); } return labelsArray; } }, calculateYscaleRange: function () { if (this.scaleOverride) { this.yScaleRange = { steps: this.scaleSteps, stepValue: this.scaleStepWidth, min: this.scaleStartValue, max: this.scaleStartValue + (this.scaleSteps * this.scaleStepWidth) }; } else { this.yScaleRange = helpers.calculateScaleRange( [this.dataRange.ymin, this.dataRange.ymax], this.chart.height, this.fontSize, this.beginAtZero, // beginAtZero, this.integersOnly); // integersOnly } }, calculateXscaleRange: function () { this.xScaleRange = helpers.calculateScaleRange( [this.dataRange.xmin, this.dataRange.xmax], this.chart.width, this.fontSize, false, // beginAtZero, true); // integersOnly }, generateYLabels: function () { this.yLabels = this.api.generateLabels( this.labelTemplate, this.yScaleRange.steps, this.yScaleRange.min, this.yScaleRange.stepValue); }, generateXLabels: function () { this.xLabels = this.api.generateLabels( this.argLabelTemplate, this.xScaleRange.steps, this.xScaleRange.min, this.xScaleRange.stepValue); }, argToString: function (arg) { return +arg + ""; }, fit: function () { // labels & padding this.calculateYscaleRange(); this.calculateXscaleRange(); this.generateYLabels(); this.generateXLabels(); var xLabelMaxWidth = helpers.longestText(this.chart.ctx, this.font, this.xLabels); var yLabelMaxWidth = helpers.longestText(this.chart.ctx, this.font, this.yLabels); this.xPadding = this.display && this.showLabels ? yLabelMaxWidth + this.padding * 2 : this.padding; var xStepWidth = Math.floor((this.chart.width - this.xPadding) / this.xScaleRange.steps); var xLabelHeight = this.fontSize * 1.5; this.xLabelRotation = xLabelMaxWidth > xStepWidth; this.xPaddingRight = this.display && this.showLabels && !this.xLabelRotation ? xLabelMaxWidth / 2 : this.padding; this.yPadding = this.display && this.showLabels ? (this.xLabelRotation ? xLabelMaxWidth : xLabelHeight) + this.padding * 2 : this.padding; }, updateBezierControlPoints: function (dataSetPoints, ease, tension) { for (var i = 0; i < dataSetPoints.length; i++) { var current = hlp.getElementOrDefault(dataSetPoints, i); var prev = hlp.getElementOrDefault(dataSetPoints, i - 1); var next = hlp.getElementOrDefault(dataSetPoints, i + 1); var obj = this.api.calculateControlPoints(prev, current, next, this.dataRange, tension); current.controlPoints = { x1: this.calculateX(obj.before.x), y1: this.calculateY(obj.before.y, ease), x2: this.calculateX(obj.after.x), y2: this.calculateY(obj.after.y, ease) }; } }, updatePoints: function (dataSetPoints, ease) { for (var i = 0; i < dataSetPoints.length; i++) { var current = dataSetPoints[i]; current.x = this.calculateX(current.arg); current.y = this.calculateY(current.value, ease); } }, calculateX: function (x) { return this.xPadding + ((x - this.xScaleRange.min) * (this.chart.width - this.xPadding - this.xPaddingRight) / (this.xScaleRange.max - this.xScaleRange.min)); }, calculateY: function (y, ease) { return this.chart.height - this.yPadding - ((y - this.yScaleRange.min) * (this.chart.height - this.yPadding - this.padding) / (this.yScaleRange.max - this.yScaleRange.min)) * (ease || 1); }, draw: function () { var ctx = this.chart.ctx, value, index; if (this.display) { var xpos1 = this.calculateX(this.xScaleRange.min); var xpos2 = this.chart.width; var ypos1 = this.calculateY(this.yScaleRange.min); var ypos2 = 0; // y axis for (index = 0, value = this.yScaleRange.min; index <= this.yScaleRange.steps; index++, value += this.yScaleRange.stepValue) { var ypos = this.calculateY(value); if (this.showLabels || this.showHorizontalLines) { // line color ctx.lineWidth = index == 0 ? this.lineWidth : this.gridLineWidth; ctx.strokeStyle = index == 0 ? this.lineColor : this.gridLineColor; ctx.beginPath(); ctx.moveTo(xpos1 - this.padding, ypos); ctx.lineTo(this.showHorizontalLines || index == 0 ? xpos2 : xpos1, ypos); ctx.stroke(); } // labels if (this.showLabels) { ctx.lineWidth = this.lineWidth; ctx.strokeStyle = this.lineColor; // text ctx.textAlign = "right"; ctx.textBaseline = "middle"; ctx.font = this.font; ctx.fillStyle = this.textColor; ctx.fillText(this.yLabels[index], xpos1 - this.padding * 1.4, ypos); } } // x axis for (index = 0, value = this.xScaleRange.min; index <= this.xScaleRange.steps; index++, value += this.xScaleRange.stepValue) { var xpos = this.calculateX(value); if (this.showLabels || this.showVerticalLines) { // line color ctx.lineWidth = index == 0 ? this.lineWidth : this.gridLineWidth; ctx.strokeStyle = index == 0 ? this.lineColor : this.gridLineColor; ctx.beginPath(); ctx.moveTo(xpos, ypos1 + this.padding); ctx.lineTo(xpos, this.showVerticalLines || index == 0 ? ypos2 : ypos1); ctx.stroke(); } // labels if (this.showLabels) { ctx.lineWidth = this.lineWidth; ctx.strokeStyle = this.lineColor; // text ctx.save(); ctx.translate(xpos, ypos1 + (this.padding * 1.4)); ctx.rotate(this.xLabelRotation ? -Math.PI / 2 : 0); ctx.textAlign = (this.xLabelRotation) ? "right" : "center"; ctx.textBaseline = (this.xLabelRotation) ? "middle" : "top"; ctx.font = this.font; ctx.fillStyle = this.textColor; ctx.fillText(this.xLabels[index], 0, 0); ctx.restore(); } } } } }); chartjs.ScatterDateScale = chartjs.ScatterNumberScale.extend({ _calculateDateScaleRange: function (valueMin, valueMax, drawingSize, fontSize) { // todo: move to global object var units = [ { u: 1, c: 1, t: 1, n: 'ms' }, { u: 1, c: 2, t: 2, n: 'ms' }, { u: 1, c: 5, t: 5, n: 'ms' }, { u: 1, c: 10, t: 10, n: 'ms' }, { u: 1, c: 20, t: 20, n: 'ms' }, { u: 1, c: 50, t: 50, n: 'ms' }, { u: 1, c: 100, t: 100, n: 'ms' }, { u: 1, c: 200, t: 200, n: 'ms' }, { u: 1, c: 500, t: 500, n: 'ms' }, { u: 1000, c: 1, t: 1000, n: 's' }, { u: 1000, c: 2, t: 2000, n: 's' }, { u: 1000, c: 5, t: 5000, n: 's' }, { u: 1000, c: 10, t: 10000, n: 's' }, { u: 1000, c: 15, t: 15000, n: 's' }, { u: 1000, c: 20, t: 20000, n: 's' }, { u: 1000, c: 30, t: 30000, n: 's' }, { u: 60000, c: 1, t: 60000, n: 'm' }, { u: 60000, c: 2, t: 120000, n: 'm' }, { u: 60000, c: 5, t: 300000, n: 'm' }, { u: 60000, c: 10, t: 600000, n: 'm' }, { u: 60000, c: 15, t: 900000, n: 'm' }, { u: 60000, c: 20, t: 1200000, n: 'm' }, { u: 60000, c: 30, t: 1800000, n: 'm' }, { u: 3600000, c: 1, t: 3600000, n: 'h' }, { u: 3600000, c: 2, t: 7200000, n: 'h' }, { u: 3600000, c: 3, t: 10800000, n: 'h' }, { u: 3600000, c: 4, t: 14400000, n: 'h' }, { u: 3600000, c: 6, t: 21600000, n: 'h' }, { u: 3600000, c: 8, t: 28800000, n: 'h' }, { u: 3600000, c: 12, t: 43200000, n: 'h' }, { u: 86400000, c: 1, t: 86400000, n: 'd' }, { u: 86400000, c: 2, t: 172800000, n: 'd' }, { u: 86400000, c: 4, t: 345600000, n: 'd' }, { u: 86400000, c: 5, t: 432000000, n: 'd' }, { u: 604800000, c: 1, t: 604800000, n: 'w' }]; var maxSteps = drawingSize / (fontSize * 3.3); var valueRange = +valueMax - valueMin, offset = this.useUtc ? 0 : new Date().getTimezoneOffset() * 60000, min = +valueMin - offset, max = +valueMax - offset; var xp = 0, f = [2, 3, 5, 7, 10]; while (valueRange / units[xp].t > maxSteps) { xp++; if (xp == units.length) { var last = units[units.length - 1]; for (var fp = 0; fp < f.length; fp++) { units.push({ u: last.u, c: last.c * f[fp], t: last.c * f[fp] * last.u, n: last.n }); } } } var stepValue = units[xp].t, start = Math.floor(min / stepValue) * stepValue, stepCount = Math.ceil((max - start) / stepValue), end = start + stepValue * stepCount; return { min: start + offset, max: end + offset, steps: stepCount, stepValue: stepValue }; }, calculateXscaleRange: function () { this.xScaleRange = this._calculateDateScaleRange( this.dataRange.xmin, this.dataRange.xmax, this.chart.width, this.fontSize ); }, argToString: function (arg) { return dateFormat(+arg, this.dateTimeFormat, this.useUtc); }, generateXLabels: function () { var graphMin = this.xScaleRange.min, stepValue = this.xScaleRange.stepValue, labelsArray = new Array(this.xScaleRange.steps + 1); helpers.each(labelsArray, function (val, index) { var value = graphMin + stepValue * index; labelsArray[index] = hlp.formatDateValue(value, this.timeFormat, this.dateFormat, this.useUtc); }, this); this.xLabels = labelsArray; } }); chartjs.ScatterDataSet = (function () { var datasetCtr = function (datasetOptions, chartOptions, chart, scale) { this.chart = chart; this.scale = scale; this.label = datasetOptions.label || null; this.strokeColor = datasetOptions.strokeColor || chartOptions.datasetStrokeColor; this.pointColor = datasetOptions.pointColor || datasetOptions.strokeColor || chartOptions.datasetStrokeColor; this.pointStrokeColor = datasetOptions.pointStrokeColor || chartOptions.datasetPointStrokeColor; this.pointDot = chartOptions.pointDot; this.pointDotRadius = chartOptions.pointDotRadius; this.pointHitDetectionRadius = chartOptions.pointHitDetectionRadius; this.pointDotStrokeWidth = chartOptions.pointDotStrokeWidth; this.scaleArgLabel = chartOptions.scaleArgLabel; this.scaleLabel = chartOptions.scaleLabel; this.scaleSizeLabel = chartOptions.scaleSizeLabel; this.points = []; }; datasetCtr.prototype.addPoint = function (x, y, r) { // default size r = arguments.length < 3 ? 1 : r; var point = this._createNewPoint(); this._setPointData(point, x, y, r); this.points.push(point); }; datasetCtr.prototype.setPointData = function (index, x, y, r) { // default size r = arguments.length < 4 ? 1 : r; var point = hlp.getElementOrDefault(this.points, index); if (point) { this._setPointData(point, x, y, r); } }; datasetCtr.prototype.removePoint = function (index) { if (index >= 0 && index < this.points.length) { this.points.splice(index, 1); } }; datasetCtr.prototype._createNewPoint = function () { return new hlp.ScatterPoint({ ctx: this.chart.ctx, datasetLabel: this.label, // point display: this.pointDot, radius: this.pointDotRadius, hitDetectionRadius: this.pointHitDetectionRadius, strokeWidth: this.pointDotStrokeWidth, // colors strokeColor: this.pointStrokeColor, highlightStroke: this.pointColor, fillColor: this.pointColor, highlightFill: this.pointStrokeColor }); }; datasetCtr.prototype._setPointData = function (point, x, y, r) { var formattedArg = this.scale.argToString(+x), formattedValue = +y + "", formattedSize = +r + ""; point.arg = +x; point.value = +y; point.size = +r; // for use in templates point.argLabel = helpers.template(this.scaleArgLabel, { value: formattedArg }), point.valueLabel = helpers.template(this.scaleLabel, { value: formattedValue }); point.sizeLabel = helpers.template(this.scaleSizeLabel, { value: formattedSize }); }; return datasetCtr; })(); chartjs.Type.extend({ name: "Scatter", defaults: defaultConfig, initialize: function (datasets) { this.hasData = false; this.datasets = []; this.scale = this._initScale(); // Compatibility layer if (datasets.datasets) { datasets = datasets.datasets; } //Iterate through each of the datasets, and build this into a property of the chart helpers.each(datasets, function (dataset) { var datasetObject = new chartjs.ScatterDataSet(dataset, this.options, this.chart, this.scale); this.datasets.push(datasetObject); this.hasData |= !!dataset.data.length; helpers.each(dataset.data, function (dataPoint) { datasetObject.addPoint(dataPoint.x, dataPoint.y, dataPoint.r || 1); }); }, this); //Set up tooltip events on the chart if (this.options.showTooltips) { helpers.bindEvents(this, this.options.tooltipEvents, function (evt) { var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; this._forEachPoint(function (point) { point.restore(['fillColor', 'strokeColor']); }); helpers.each(activePoints, function (activePoint) { activePoint.fillColor = activePoint.highlightFill; activePoint.strokeColor = activePoint.highlightStroke; }); this.showTooltip(activePoints); }); } var dataRange = this._calculateRange(); this.scale.setDataRange(dataRange); this.update(); }, _initScale: function () { var scaleOptions = { chart: this.chart, textColor: this.options.scaleFontColor, fontSize: this.options.scaleFontSize, fontStyle: this.options.scaleFontStyle, fontFamily: this.options.scaleFontFamily, labelTemplate: this.options.scaleLabel, argLabelTemplate: this.options.scaleArgLabel, showLabels: this.options.scaleShowLabels, beginAtZero: this.options.scaleBeginAtZero, integersOnly: this.options.scaleIntegersOnly, gridLineWidth: (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, gridLineColor: (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", showHorizontalLines: this.options.scaleShowHorizontalLines, showVerticalLines: this.options.scaleShowVerticalLines, lineWidth: this.options.scaleLineWidth, lineColor: this.options.scaleLineColor, display: this.options.showScale, // range scaleOverride: this.options.scaleOverride, scaleSteps: this.options.scaleSteps, scaleStepWidth: this.options.scaleStepWidth, scaleStartValue: this.options.scaleStartValue, // dates useUtc: this.options.useUtc, dateFormat: this.options.scaleDateFormat, timeFormat: this.options.scaleTimeFormat, dateTimeFormat: this.options.scaleDateTimeFormat }; return this.options.scaleType === "date" ? new chartjs.ScatterDateScale(scaleOptions) : new chartjs.ScatterNumberScale(scaleOptions); }, // helpers getPointsAtEvent: function (e) { var pointsArray = [], eventPosition = helpers.getRelativePosition(e); helpers.each(this.datasets, function (dataset) { helpers.each(dataset.points, function (point) { if (point.inRange(eventPosition.x, eventPosition.y)) pointsArray.push(point); }); }, this); return pointsArray; }, showTooltip: function (elements) { this.draw(); if (elements.length > 0) { var firstElement = elements[0]; var tooltipPosition = firstElement.tooltipPosition(); if (elements.length == 1) { new chartjs.Tooltip({ x: Math.round(tooltipPosition.x), y: Math.round(tooltipPosition.y), xPadding: this.options.tooltipXPadding, yPadding: this.options.tooltipYPadding, fillColor: this.options.tooltipFillColor, textColor: this.options.tooltipFontColor, fontFamily: this.options.tooltipFontFamily, fontStyle: this.options.tooltipFontStyle, fontSize: this.options.tooltipFontSize, caretHeight: this.options.tooltipCaretSize, cornerRadius: this.options.tooltipCornerRadius, text: helpers.template(this.options.tooltipTemplate, firstElement), chart: this.chart, custom: this.options.customTooltips }).draw(); } else { var tooltipLabels = [], tooltipColors = []; helpers.each(elements, function (point) { tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, point)); tooltipColors.push({ fill: point._saved.fillColor || point.fillColor, stroke: point._saved.strokeColor || point.strokeColor }); }, this); new chartjs.MultiTooltip({ x: Math.round(tooltipPosition.x), y: Math.round(tooltipPosition.y), xPadding: this.options.tooltipXPadding, yPadding: this.options.tooltipYPadding, xOffset: this.options.tooltipXOffset, fillColor: this.options.tooltipFillColor, textColor: this.options.tooltipFontColor, fontFamily: this.options.tooltipFontFamily, fontStyle: this.options.tooltipFontStyle, fontSize: this.options.tooltipFontSize, titleTextColor: this.options.tooltipTitleFontColor, titleFontFamily: this.options.tooltipTitleFontFamily, titleFontStyle: this.options.tooltipTitleFontStyle, titleFontSize: this.options.tooltipTitleFontSize, cornerRadius: this.options.tooltipCornerRadius, labels: tooltipLabels, legendColors: tooltipColors, legendColorBackground: this.options.multiTooltipKeyBackground, title: '', chart: this.chart, ctx: this.chart.ctx, custom: this.options.customTooltips }).draw(); } } return this; }, _forEachPoint: function (callback) { helpers.each(this.datasets, function (dataset) { helpers.each(dataset.points, callback, this); }, this); }, _forEachDataset: function (callback) { helpers.each(this.datasets, callback, this); }, _calculateRange: function () { var xmin = undefined, xmax = undefined, ymin = undefined, ymax = undefined; this._forEachPoint(function (point) { // min x if (xmin === undefined || point.arg < xmin) { xmin = point.arg; } // max x if (xmax === undefined || point.arg > xmax) { xmax = point.arg; } // min y if (ymin === undefined || point.value < ymin) { ymin = point.value; } // max y if (ymax === undefined || point.value > ymax) { ymax = point.value; } }); return { xmin: xmin, xmax: xmax, ymin: ymin, ymax: ymax } }, _drawMessage: function (message) { var ctx = this.chart.ctx, width = this.chart.width, height = this.chart.height, fontSize = this.options.scaleFontSize, fontStyle = this.options.scaleFontStyle, fontFamily = this.options.scaleFontFamily, font = helpers.fontString(fontSize, fontStyle, fontFamily); // text ctx.save(); ctx.translate(width / 2, height / 2); ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.font = font; ctx.fillStyle = this.options.scaleFontColor; ctx.fillText(message, 0, 0); ctx.restore(); }, _drawLine: function (dataset) { var ctx = this.chart.ctx, prev = undefined; ctx.lineJoin = "round"; ctx.lineWidth = this.options.datasetStrokeWidth; ctx.strokeStyle = dataset.strokeColor || this.options.datasetStrokeColor; ctx.beginPath(); helpers.each(dataset.points, function (point, index) { if (index === 0) { ctx.moveTo(point.x, point.y); } else { if (this.options.bezierCurve) { ctx.bezierCurveTo( prev.controlPoints.x2, prev.controlPoints.y2, point.controlPoints.x1, point.controlPoints.y1, point.x, point.y); } else { ctx.lineTo(point.x, point.y); } } prev = point; }, this); ctx.stroke(); // debug //if (this.options.bezierCurve) { // ctx.lineWidth = 0.3; // helpers.each(dataset.points, function(point) { // ctx.beginPath(); // ctx.moveTo(point.controlPoints.x1, point.controlPoints.y1); // ctx.lineTo(point.x, point.y); // ctx.lineTo(point.controlPoints.x2, point.controlPoints.y2); // ctx.stroke(); // }); //} }, update: function () { var dataRange = this._calculateRange(); this.scale.setDataRange(dataRange); this.render(); }, draw: function (ease) { if (this.hasData) { // update view params this.scale.fit(); this._forEachDataset(function (dataset) { this.scale.updatePoints(dataset.points, ease); if (this.options.bezierCurve) { this.scale.updateBezierControlPoints(dataset.points, ease, this.options.bezierCurveTension); } }); // draw this.clear(); this.scale.draw(); // draw lines if (this.options.datasetStroke) { helpers.each(this.datasets, this._drawLine, this); } // draw points if (this.options.pointDot) { this._forEachPoint(function (point) { point.draw(); }); } } else { this.clear(); this._drawMessage(this.options.emptyDataMessage); } } }); }));