From: Evert Timberg Date: Sat, 13 Jun 2015 13:51:28 +0000 (-0400) Subject: Move some of the radial linear scale functions into the radialLinear scale X-Git-Tag: 2.0.0-alpha2~4^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=30019dfddd123621d8ed77e8ce0277f3ff8d35e5;p=thirdparty%2FChart.js.git Move some of the radial linear scale functions into the radialLinear scale --- diff --git a/src/charts/chart.polarArea.js b/src/charts/chart.polarArea.js index 4c6277488..72f4b8f76 100644 --- a/src/charts/chart.polarArea.js +++ b/src/charts/chart.polarArea.js @@ -1,400 +1,383 @@ (function() { - "use strict"; - - var root = this, - Chart = root.Chart, - //Cache a local reference to Chart.helpers - helpers = Chart.helpers; - - var defaultConfig = { - - scale: { - type: "radialLinear", - display: true, - - //Boolean - Whether to animate scaling the chart from the centre - animate: false, - - lineArc: true, - - // grid line settings - gridLines: { - show: true, - color: "rgba(0, 0, 0, 0.05)", - lineWidth: 1, - }, - - // scale numbers - beginAtZero: true, - - // label settings - labels: { - show: true, - template: "<%=value.toLocaleString()%>", - fontSize: 12, - fontStyle: "normal", - fontColor: "#666", - fontFamily: "Helvetica Neue", - - //Boolean - Show a backdrop to the scale label - showLabelBackdrop: true, - - //String - The colour of the label backdrop - backdropColor: "rgba(255,255,255,0.75)", - - //Number - The backdrop padding above & below the label in pixels - backdropPaddingY: 2, - - //Number - The backdrop padding to the side of the label in pixels - backdropPaddingX: 2, - } - }, - - //Boolean - Whether to animate the rotation of the chart - animateRotate: true, - }; - - - Chart.Type.extend({ - //Passing in a name registers this chart in the Chart namespace - name: "PolarArea", - //Providing a defaults will also register the deafults in the chart namespace - defaults: defaultConfig, - //Initialize is fired when the chart is initialized - Data is passed in as a parameter - //Config is automatically merged by the core of Chart.js, and is available at this.options - initialize: function() { - - // Scale setup - var self = this; - var ScaleClass = Chart.scales.getScaleConstructor(this.options.scale.type); - this.scale = new ScaleClass({ - options: this.options.scale, - lineArc: true, - width: this.chart.width, - height: this.chart.height, - xCenter: this.chart.width / 2, - yCenter: this.chart.height / 2, - ctx: this.chart.ctx, - valuesCount: this.data.length, - calculateRange: function() { - this.min = null; - this.max = null; - - helpers.each(self.data.datasets[0].data, function(value) { - if (this.min === null) { - this.min = value; - } else if (value < this.min) { - this.min = value; - } - - if (this.max === null) { - this.max = value; - } else if (value > this.max) { - this.max = value; - } - }, this); - } - }); - - helpers.bindEvents(this, this.options.events, this.events); - - //Set up tooltip events on the chart - helpers.bindEvents(this, this.options.events, this.events); - - //Create a new bar for each piece of data - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - dataset.metaData = []; - helpers.each(dataset.data, function(dataPoint, index) { - dataset.metaData.push(new Chart.Arc({ - _chart: this.chart, - _datasetIndex: datasetIndex, - _index: index, - _model: {} - })); - }, this); - }, this); - - // Create tooltip instance exclusively for this chart with some defaults. - this.tooltip = new Chart.Tooltip({ - _chart: this.chart, - _data: this.data, - _options: this.options, - }, this); - - // Fit the scale before we animate - this.updateScaleRange(); - this.scale.calculateRange(); - Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); - - // so that we animate nicely - this.resetElements(); - - // Update the chart with the latest data. - this.update(); - - }, - updateScaleRange: function() { - helpers.extend(this.scale, { - size: helpers.min([this.chart.width, this.chart.height]), - xCenter: this.chart.width / 2, - yCenter: this.chart.height / 2 - }); - }, - resetElements: function() { - var circumference = 1 / this.data.datasets[0].data.length * 2; - - // Map new data to data points - helpers.each(this.data.datasets[0].metaData, function(slice, index) { - - var value = this.data.datasets[0].data[index]; - - helpers.extend(slice, { - _index: index, - _model: { - x: this.chart.width / 2, - y: this.chart.height / 2, - innerRadius: 0, - outerRadius: 0, - startAngle: Math.PI * -0.5, - endAngle: Math.PI * -0.5, - - backgroundColor: slice.custom && slice.custom.backgroundColor ? slice.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].backgroundColor, index, this.options.elements.arc.backgroundColor), - hoverBackgroundColor: slice.custom && slice.custom.hoverBackgroundColor ? slice.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].hoverBackgroundColor, index, this.options.elements.arc.hoverBackgroundColor), - borderWidth: slice.custom && slice.custom.borderWidth ? slice.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.data.datasets[0].borderWidth, index, this.options.elements.arc.borderWidth), - borderColor: slice.custom && slice.custom.borderColor ? slice.custom.borderColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].borderColor, index, this.options.elements.arc.borderColor), - - label: helpers.getValueAtIndexOrDefault(this.data.datasets[0].labels, index, this.data.datasets[0].labels[index]) - }, - }); - - slice.pivot(); - }, this); - }, - update: function(animationDuration) { - - this.updateScaleRange(); - this.scale.calculateRange(); - this.scale.generateTicks(); - this.scale.buildYLabels(); - - Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); - - var circumference = 1 / this.data.datasets[0].data.length * 2; - - // Map new data to data points - helpers.each(this.data.datasets[0].metaData, function(slice, index) { - - var value = this.data.datasets[0].data[index]; - - var startAngle = (-0.5 * Math.PI) + (Math.PI * circumference) * index; - var endAngle = startAngle + (circumference * Math.PI); - - helpers.extend(slice, { - _index: index, - _model: { - x: this.chart.width / 2, - y: this.chart.height / 2, - innerRadius: 0, - outerRadius: this.scale.calculateCenterOffset(value), - startAngle: startAngle, - endAngle: endAngle, - - backgroundColor: slice.custom && slice.custom.backgroundColor ? slice.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].backgroundColor, index, this.options.elements.arc.backgroundColor), - hoverBackgroundColor: slice.custom && slice.custom.hoverBackgroundColor ? slice.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].hoverBackgroundColor, index, this.options.elements.arc.hoverBackgroundColor), - borderWidth: slice.custom && slice.custom.borderWidth ? slice.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.data.datasets[0].borderWidth, index, this.options.elements.arc.borderWidth), - borderColor: slice.custom && slice.custom.borderColor ? slice.custom.borderColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].borderColor, index, this.options.elements.arc.borderColor), - - label: helpers.getValueAtIndexOrDefault(this.data.datasets[0].labels, index, this.data.datasets[0].labels[index]) - }, - }); - slice.pivot(); - - console.log(slice); - - }, this); - - this.render(animationDuration); - }, - draw: function(ease) { - var easingDecimal = ease || 1; - - this.clear(); - - helpers.each(this.data.datasets[0].metaData, function(slice, index) { - slice.transition(easingDecimal).draw(); - }, this); - - this.scale.draw(); - - this.tooltip.transition(easingDecimal).draw(); - }, - events: function(e) { - - // If exiting chart - if (e.type == 'mouseout') { - return this; - } - - this.lastActive = this.lastActive || []; - - // Find Active Elements - this.active = function() { - switch (this.options.hover.mode) { - case 'single': - return this.getSliceAtEvent(e); - case 'label': - return this.getSlicesAtEvent(e); - case 'dataset': - return this.getDatasetAtEvent(e); - default: - return e; - } - }.call(this); - - // On Hover hook - if (this.options.hover.onHover) { - this.options.hover.onHover.call(this, this.active); - } - - if (e.type == 'mouseup' || e.type == 'click') { - if (this.options.onClick) { - this.options.onClick.call(this, e, this.active); - } - } - - var dataset; - var index; - // Remove styling for last active (even if it may still be active) - if (this.lastActive.length) { - switch (this.options.hover.mode) { - case 'single': - dataset = this.data.datasets[this.lastActive[0]._datasetIndex]; - index = this.lastActive[0]._index; - - this.lastActive[0]._model.backgroundColor = this.lastActive[0].custom && this.lastActive[0].custom.backgroundColor ? this.lastActive[0].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, this.options.elements.arc.backgroundColor); - this.lastActive[0]._model.borderColor = this.lastActive[0].custom && this.lastActive[0].custom.borderColor ? this.lastActive[0].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, this.options.elements.arc.borderColor); - this.lastActive[0]._model.borderWidth = this.lastActive[0].custom && this.lastActive[0].custom.borderWidth ? this.lastActive[0].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.options.elements.arc.borderWidth); - break; - case 'label': - for (var i = 0; i < this.lastActive.length; i++) { - dataset = this.data.datasets[this.lastActive[i]._datasetIndex]; - index = this.lastActive[i]._index; - - this.lastActive[i]._model.backgroundColor = this.lastActive[i].custom && this.lastActive[i].custom.backgroundColor ? this.lastActive[i].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, this.options.elements.arc.backgroundColor); - this.lastActive[i]._model.borderColor = this.lastActive[i].custom && this.lastActive[i].custom.borderColor ? this.lastActive[i].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, this.options.elements.arc.borderColor); - this.lastActive[i]._model.borderWidth = this.lastActive[i].custom && this.lastActive[i].custom.borderWidth ? this.lastActive[i].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.options.elements.arc.borderWidth); - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - // Built in hover styling - if (this.active.length && this.options.hover.mode) { - switch (this.options.hover.mode) { - case 'single': - dataset = this.data.datasets[this.active[0]._datasetIndex]; - index = this.active[0]._index; - - this.active[0]._model.radius = this.active[0].custom && this.active[0].custom.hoverRadius ? this.active[0].custom.hoverRadius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.active[0]._model.radius + 1); - this.active[0]._model.backgroundColor = this.active[0].custom && this.active[0].custom.hoverBackgroundColor ? this.active[0].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(this.active[0]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - this.active[0]._model.borderColor = this.active[0].custom && this.active[0].custom.hoverBorderColor ? this.active[0].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(this.active[0]._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - this.active[0]._model.borderWidth = this.active[0].custom && this.active[0].custom.hoverBorderWidth ? this.active[0].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.active[0]._model.borderWidth); - break; - case 'label': - for (var i = 0; i < this.active.length; i++) { - dataset = this.data.datasets[this.active[i]._datasetIndex]; - index = this.active[i]._index; - - this.active[i]._model.radius = this.active[i].custom && this.active[i].custom.hoverRadius ? this.active[i].custom.hoverRadius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.active[i]._model.radius + 1); - this.active[i]._model.backgroundColor = this.active[i].custom && this.active[i].custom.hoverBackgroundColor ? this.active[i].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(this.active[i]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - this.active[i]._model.borderColor = this.active[i].custom && this.active[i].custom.hoverBorderColor ? this.active[i].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(this.active[i]._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - this.active[i]._model.borderWidth = this.active[i].custom && this.active[i].custom.hoverBorderWidth ? this.active[i].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.active[i]._model.borderWidth); - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - - // Built in Tooltips - if (this.options.tooltips.enabled) { - - // The usual updates - this.tooltip.initialize(); - - // Active - if (this.active.length) { - this.tooltip._model.opacity = 1; - - helpers.extend(this.tooltip, { - _active: this.active, - }); - - this.tooltip.update(); - } else { - // Inactive - this.tooltip._model.opacity = 0; - } - } - - - // Hover animations - this.tooltip.pivot(); - - if (!this.animating) { - var changed; - - helpers.each(this.active, function(element, index) { - if (element !== this.lastActive[index]) { - changed = true; - } - }, this); - - // If entering, leaving, or changing elements, animate the change via pivot - if ((!this.lastActive.length && this.active.length) || - (this.lastActive.length && !this.active.length) || - (this.lastActive.length && this.active.length && changed)) { - - this.stop(); - this.render(this.options.hover.animationDuration); - } - } - - // Remember Last Active - this.lastActive = this.active; - return this; - }, - getSliceAtEvent: function(e) { - var elements = []; - - var location = helpers.getRelativePosition(e); - - this.eachElement(function(slice, index) { - if (slice.inRange(location.x, location.y)) { - elements.push(slice); - } - }, this); - return elements; - }, - /*getSlicesAtEvent: function(e) { - var elements = []; - - var location = helpers.getRelativePosition(e); - - this.eachElement(function(slice, index) { - if (slice.inGroupRange(location.x, location.y)) { - elements.push(slice); - } - }, this); - return elements; - },*/ - }); + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + + scale: { + type: "radialLinear", + display: true, + + //Boolean - Whether to animate scaling the chart from the centre + animate: false, + + lineArc: true, + + // grid line settings + gridLines: { + show: true, + color: "rgba(0, 0, 0, 0.05)", + lineWidth: 1, + }, + + // scale numbers + beginAtZero: true, + + // label settings + labels: { + show: true, + template: "<%=value.toLocaleString()%>", + fontSize: 12, + fontStyle: "normal", + fontColor: "#666", + fontFamily: "Helvetica Neue", + + //Boolean - Show a backdrop to the scale label + showLabelBackdrop: true, + + //String - The colour of the label backdrop + backdropColor: "rgba(255,255,255,0.75)", + + //Number - The backdrop padding above & below the label in pixels + backdropPaddingY: 2, + + //Number - The backdrop padding to the side of the label in pixels + backdropPaddingX: 2, + } + }, + + //Boolean - Whether to animate the rotation of the chart + animateRotate: true, + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "PolarArea", + //Providing a defaults will also register the deafults in the chart namespace + defaults: defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function() { + + // Scale setup + var self = this; + var ScaleClass = Chart.scales.getScaleConstructor(this.options.scale.type); + this.scale = new ScaleClass({ + options: this.options.scale, + lineArc: true, + width: this.chart.width, + height: this.chart.height, + xCenter: this.chart.width / 2, + yCenter: this.chart.height / 2, + ctx: this.chart.ctx, + valuesCount: this.data.length, + data: this.data + }); + + helpers.bindEvents(this, this.options.events, this.events); + + //Set up tooltip events on the chart + helpers.bindEvents(this, this.options.events, this.events); + + //Create a new bar for each piece of data + helpers.each(this.data.datasets, function(dataset, datasetIndex) { + dataset.metaData = []; + helpers.each(dataset.data, function(dataPoint, index) { + dataset.metaData.push(new Chart.Arc({ + _chart: this.chart, + _datasetIndex: datasetIndex, + _index: index, + _model: {} + })); + }, this); + }, this); + + // Create tooltip instance exclusively for this chart with some defaults. + this.tooltip = new Chart.Tooltip({ + _chart: this.chart, + _data: this.data, + _options: this.options, + }, this); + + // Fit the scale before we animate + this.updateScaleRange(); + this.scale.calculateRange(); + Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); + + // so that we animate nicely + this.resetElements(); + + // Update the chart with the latest data. + this.update(); + + }, + updateScaleRange: function() { + helpers.extend(this.scale, { + size: helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width / 2, + yCenter: this.chart.height / 2 + }); + }, + resetElements: function() { + var circumference = 1 / this.data.datasets[0].data.length * 2; + + // Map new data to data points + helpers.each(this.data.datasets[0].metaData, function(slice, index) { + + var value = this.data.datasets[0].data[index]; + + helpers.extend(slice, { + _index: index, + _model: { + x: this.chart.width / 2, + y: this.chart.height / 2, + innerRadius: 0, + outerRadius: 0, + startAngle: Math.PI * -0.5, + endAngle: Math.PI * -0.5, + + backgroundColor: slice.custom && slice.custom.backgroundColor ? slice.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].backgroundColor, index, this.options.elements.arc.backgroundColor), + hoverBackgroundColor: slice.custom && slice.custom.hoverBackgroundColor ? slice.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].hoverBackgroundColor, index, this.options.elements.arc.hoverBackgroundColor), + borderWidth: slice.custom && slice.custom.borderWidth ? slice.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.data.datasets[0].borderWidth, index, this.options.elements.arc.borderWidth), + borderColor: slice.custom && slice.custom.borderColor ? slice.custom.borderColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].borderColor, index, this.options.elements.arc.borderColor), + + label: helpers.getValueAtIndexOrDefault(this.data.datasets[0].labels, index, this.data.datasets[0].labels[index]) + }, + }); + + slice.pivot(); + }, this); + }, + update: function(animationDuration) { + + this.updateScaleRange(); + this.scale.calculateRange(); + this.scale.generateTicks(); + this.scale.buildYLabels(); + + Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); + + var circumference = 1 / this.data.datasets[0].data.length * 2; + + // Map new data to data points + helpers.each(this.data.datasets[0].metaData, function(slice, index) { + + var value = this.data.datasets[0].data[index]; + + var startAngle = (-0.5 * Math.PI) + (Math.PI * circumference) * index; + var endAngle = startAngle + (circumference * Math.PI); + + helpers.extend(slice, { + _index: index, + _model: { + x: this.chart.width / 2, + y: this.chart.height / 2, + innerRadius: 0, + outerRadius: this.scale.calculateCenterOffset(value), + startAngle: startAngle, + endAngle: endAngle, + + backgroundColor: slice.custom && slice.custom.backgroundColor ? slice.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].backgroundColor, index, this.options.elements.arc.backgroundColor), + hoverBackgroundColor: slice.custom && slice.custom.hoverBackgroundColor ? slice.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].hoverBackgroundColor, index, this.options.elements.arc.hoverBackgroundColor), + borderWidth: slice.custom && slice.custom.borderWidth ? slice.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.data.datasets[0].borderWidth, index, this.options.elements.arc.borderWidth), + borderColor: slice.custom && slice.custom.borderColor ? slice.custom.borderColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].borderColor, index, this.options.elements.arc.borderColor), + + label: helpers.getValueAtIndexOrDefault(this.data.datasets[0].labels, index, this.data.datasets[0].labels[index]) + }, + }); + slice.pivot(); + + console.log(slice); + + }, this); + + this.render(animationDuration); + }, + draw: function(ease) { + var easingDecimal = ease || 1; + + this.clear(); + + helpers.each(this.data.datasets[0].metaData, function(slice, index) { + slice.transition(easingDecimal).draw(); + }, this); + + this.scale.draw(); + + this.tooltip.transition(easingDecimal).draw(); + }, + events: function(e) { + + // If exiting chart + if (e.type == 'mouseout') { + return this; + } + + this.lastActive = this.lastActive || []; + + // Find Active Elements + this.active = function() { + switch (this.options.hover.mode) { + case 'single': + return this.getSliceAtEvent(e); + case 'label': + return this.getSlicesAtEvent(e); + case 'dataset': + return this.getDatasetAtEvent(e); + default: + return e; + } + }.call(this); + + // On Hover hook + if (this.options.hover.onHover) { + this.options.hover.onHover.call(this, this.active); + } + + if (e.type == 'mouseup' || e.type == 'click') { + if (this.options.onClick) { + this.options.onClick.call(this, e, this.active); + } + } + + var dataset; + var index; + // Remove styling for last active (even if it may still be active) + if (this.lastActive.length) { + switch (this.options.hover.mode) { + case 'single': + dataset = this.data.datasets[this.lastActive[0]._datasetIndex]; + index = this.lastActive[0]._index; + + this.lastActive[0]._model.backgroundColor = this.lastActive[0].custom && this.lastActive[0].custom.backgroundColor ? this.lastActive[0].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, this.options.elements.arc.backgroundColor); + this.lastActive[0]._model.borderColor = this.lastActive[0].custom && this.lastActive[0].custom.borderColor ? this.lastActive[0].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, this.options.elements.arc.borderColor); + this.lastActive[0]._model.borderWidth = this.lastActive[0].custom && this.lastActive[0].custom.borderWidth ? this.lastActive[0].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.options.elements.arc.borderWidth); + break; + case 'label': + for (var i = 0; i < this.lastActive.length; i++) { + dataset = this.data.datasets[this.lastActive[i]._datasetIndex]; + index = this.lastActive[i]._index; + + this.lastActive[i]._model.backgroundColor = this.lastActive[i].custom && this.lastActive[i].custom.backgroundColor ? this.lastActive[i].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, this.options.elements.arc.backgroundColor); + this.lastActive[i]._model.borderColor = this.lastActive[i].custom && this.lastActive[i].custom.borderColor ? this.lastActive[i].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, this.options.elements.arc.borderColor); + this.lastActive[i]._model.borderWidth = this.lastActive[i].custom && this.lastActive[i].custom.borderWidth ? this.lastActive[i].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.options.elements.arc.borderWidth); + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + // Built in hover styling + if (this.active.length && this.options.hover.mode) { + switch (this.options.hover.mode) { + case 'single': + dataset = this.data.datasets[this.active[0]._datasetIndex]; + index = this.active[0]._index; + + this.active[0]._model.radius = this.active[0].custom && this.active[0].custom.hoverRadius ? this.active[0].custom.hoverRadius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.active[0]._model.radius + 1); + this.active[0]._model.backgroundColor = this.active[0].custom && this.active[0].custom.hoverBackgroundColor ? this.active[0].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(this.active[0]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); + this.active[0]._model.borderColor = this.active[0].custom && this.active[0].custom.hoverBorderColor ? this.active[0].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(this.active[0]._model.borderColor).saturate(0.5).darken(0.1).rgbString()); + this.active[0]._model.borderWidth = this.active[0].custom && this.active[0].custom.hoverBorderWidth ? this.active[0].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.active[0]._model.borderWidth); + break; + case 'label': + for (var i = 0; i < this.active.length; i++) { + dataset = this.data.datasets[this.active[i]._datasetIndex]; + index = this.active[i]._index; + + this.active[i]._model.radius = this.active[i].custom && this.active[i].custom.hoverRadius ? this.active[i].custom.hoverRadius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.active[i]._model.radius + 1); + this.active[i]._model.backgroundColor = this.active[i].custom && this.active[i].custom.hoverBackgroundColor ? this.active[i].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(this.active[i]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); + this.active[i]._model.borderColor = this.active[i].custom && this.active[i].custom.hoverBorderColor ? this.active[i].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(this.active[i]._model.borderColor).saturate(0.5).darken(0.1).rgbString()); + this.active[i]._model.borderWidth = this.active[i].custom && this.active[i].custom.hoverBorderWidth ? this.active[i].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.active[i]._model.borderWidth); + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + + // Built in Tooltips + if (this.options.tooltips.enabled) { + + // The usual updates + this.tooltip.initialize(); + + // Active + if (this.active.length) { + this.tooltip._model.opacity = 1; + + helpers.extend(this.tooltip, { + _active: this.active, + }); + + this.tooltip.update(); + } else { + // Inactive + this.tooltip._model.opacity = 0; + } + } + + + // Hover animations + this.tooltip.pivot(); + + if (!this.animating) { + var changed; + + helpers.each(this.active, function(element, index) { + if (element !== this.lastActive[index]) { + changed = true; + } + }, this); + + // If entering, leaving, or changing elements, animate the change via pivot + if ((!this.lastActive.length && this.active.length) || + (this.lastActive.length && !this.active.length) || + (this.lastActive.length && this.active.length && changed)) { + + this.stop(); + this.render(this.options.hover.animationDuration); + } + } + + // Remember Last Active + this.lastActive = this.active; + return this; + }, + getSliceAtEvent: function(e) { + var elements = []; + + var location = helpers.getRelativePosition(e); + + this.eachElement(function(slice, index) { + if (slice.inRange(location.x, location.y)) { + elements.push(slice); + } + }, this); + return elements; + }, + /*getSlicesAtEvent: function(e) { + var elements = []; + + var location = helpers.getRelativePosition(e); + + this.eachElement(function(slice, index) { + if (slice.inGroupRange(location.x, location.y)) { + elements.push(slice); + } + }, this); + return elements; + },*/ + }); }).call(this); diff --git a/src/charts/chart.radar.js b/src/charts/chart.radar.js index 57c735136..2fcf88ba0 100644 --- a/src/charts/chart.radar.js +++ b/src/charts/chart.radar.js @@ -1,517 +1,496 @@ (function() { - "use strict"; + "use strict"; - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; - Chart.Type.extend({ - name: "Radar", - defaults: { + Chart.Type.extend({ + name: "Radar", + defaults: { - scale: { - type: "radialLinear", - display: true, + scale: { + type: "radialLinear", + display: true, - //Boolean - Whether to animate scaling the chart from the centre - animate: false, + //Boolean - Whether to animate scaling the chart from the centre + animate: false, - lineArc: false, + lineArc: false, - // grid line settings - gridLines: { - show: true, - color: "rgba(0, 0, 0, 0.05)", - lineWidth: 1, - }, + // grid line settings + gridLines: { + show: true, + color: "rgba(0, 0, 0, 0.05)", + lineWidth: 1, + }, - angleLines: { - show: true, - color: "rgba(0,0,0,.1)", - lineWidth: 1 - }, + angleLines: { + show: true, + color: "rgba(0,0,0,.1)", + lineWidth: 1 + }, - // scale numbers - beginAtZero: true, + // scale numbers + beginAtZero: true, - // label settings - labels: { - show: true, - template: "<%=value.toLocaleString()%>", - fontSize: 12, - fontStyle: "normal", - fontColor: "#666", - fontFamily: "Helvetica Neue", - - //Boolean - Show a backdrop to the scale label - showLabelBackdrop: true, - - //String - The colour of the label backdrop - backdropColor: "rgba(255,255,255,0.75)", - - //Number - The backdrop padding above & below the label in pixels - backdropPaddingY: 2, - - //Number - The backdrop padding to the side of the label in pixels - backdropPaddingX: 2, - }, - - pointLabels: { - //String - Point label font declaration - fontFamily: "'Arial'", - - //String - Point label font weight - fontStyle: "normal", - - //Number - Point label font size in pixels - fontSize: 10, - - //String - Point label font colour - fontColor: "#666", - }, - }, - - elements: { - line: { - tension: 0, // no bezier in radar - } - }, - - //String - A legend template - legendTemplate: "" - - }, - - initialize: function() { - - // Events - helpers.bindEvents(this, this.options.events, this.events); - - // Create a new line and its points for each dataset and piece of data - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - - dataset.metaDataset = new Chart.Line({ - _chart: this.chart, - _datasetIndex: datasetIndex, - _points: dataset.metaData, - _loop: true - }); - - dataset.metaData = []; - - helpers.each(dataset.data, function(dataPoint, index) { - dataset.metaData.push(new Chart.Point({ - _datasetIndex: datasetIndex, - _index: index, - _chart: this.chart, - _model: { - x: 0, //xScale.getPixelForValue(null, index, true), - y: 0, //this.chartArea.bottom, - }, - })); - - }, this); - }, this); - - // Build the scale. - this.buildScale(); - - // Create tooltip instance exclusively for this chart with some defaults. - this.tooltip = new Chart.Tooltip({ - _chart: this.chart, - _data: this.data, - _options: this.options, - }, this); - - // Need to fit scales before we reset elements. - Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); - - // Reset so that we animation from the baseline - this.resetElements(); - - // Update that shiz - this.update(); - }, - nextPoint: function(collection, index) { - return collection[index + 1] || collection[0]; - }, - previousPoint: function(collection, index) { - return collection[index - 1] || collection[collection.length - 1]; - }, - resetElements: function() { - - // Update the points - this.eachElement(function(point, index, dataset, datasetIndex) { - helpers.extend(point, { - // Utility - _chart: this.chart, - _datasetIndex: datasetIndex, - _index: index, - _scale: this.scale, - - // Desired view properties - _model: { - x: this.scale.xCenter, - y: this.scale.yCenter, - - // Appearance - tension: point.custom && point.custom.tension ? point.custom.tension : this.options.elements.line.tension, - radius: point.custom && point.custom.radius ? point.custom.pointRadius : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointRadius, index, this.options.elements.point.radius), - backgroundColor: point.custom && point.custom.backgroundColor ? point.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBackgroundColor, index, this.options.elements.point.backgroundColor), - borderColor: point.custom && point.custom.borderColor ? point.custom.borderColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBorderColor, index, this.options.elements.point.borderColor), - borderWidth: point.custom && point.custom.borderWidth ? point.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBorderWidth, index, this.options.elements.point.borderWidth), - skip: this.data.datasets[datasetIndex].data[index] === null, - - // Tooltip - hitRadius: point.custom && point.custom.hitRadius ? point.custom.hitRadius : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].hitRadius, index, this.options.elements.point.hitRadius), - }, - }); - }, this); - - // Update control points for the bezier curve - this.eachElement(function(point, index, dataset, datasetIndex) { - var controlPoints = helpers.splineCurve( - this.previousPoint(dataset, index)._model, - point._model, - this.nextPoint(dataset, index)._model, - point._model.tension - ); - - point._model.controlPointPreviousX = this.scale.xCenter; - point._model.controlPointPreviousY = this.scale.yCenter; - point._model.controlPointNextX = this.scale.xCenter; - point._model.controlPointNextY = this.scale.yCenter; - - // Now pivot the point for animation - point.pivot(); - }, this); - }, - update: function(animationDuration) { - Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); - - // Update the lines - this.eachDataset(function(dataset, datasetIndex) { - var scaleBase; - - if (this.scale.min < 0 && this.scale.max < 0) { - scaleBase = this.scale.getPointPosition(0, this.scale.max); - } else if (this.scale.min > 0 && this.scale.max > 0) { - scaleBase = this.scale.getPointPosition(0, this.scale.min); - } else { - scaleBase = this.scale.getPointPosition(0, 0); - } - - helpers.extend(dataset.metaDataset, { - // Utility - _datasetIndex: datasetIndex, - - // Data - _children: dataset.metaData, - - // Model - _model: { - // Appearance - tension: dataset.tension || this.options.elements.line.tension, - backgroundColor: dataset.backgroundColor || this.options.elements.line.backgroundColor, - borderWidth: dataset.borderWidth || this.options.elements.line.borderWidth, - borderColor: dataset.borderColor || this.options.elements.line.borderColor, - fill: dataset.fill !== undefined ? dataset.fill : this.options.elements.line.fill, // use the value from the dataset if it was provided. else fall back to the default - skipNull: dataset.skipNull !== undefined ? dataset.skipNull : this.options.elements.line.skipNull, - drawNull: dataset.drawNull !== undefined ? dataset.drawNull : this.options.elements.line.drawNull, - - // Scale - scaleTop: this.scale.top, - scaleBottom: this.scale.bottom, - scaleZero: scaleBase, - }, - }); - - dataset.metaDataset.pivot(); - }); - - // Update the points - this.eachElement(function(point, index, dataset, datasetIndex) { - var pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(this.data.datasets[datasetIndex].data[index])); - - helpers.extend(point, { - // Utility - _chart: this.chart, - _datasetIndex: datasetIndex, - _index: index, - - // Desired view properties - _model: { - x: pointPosition.x, // value not used in dataset scale, but we want a consistent API between scales - y: pointPosition.y, - - // Appearance - tension: point.custom && point.custom.tension ? point.custom.tension : this.options.elements.line.tension, - radius: point.custom && point.custom.radius ? point.custom.pointRadius : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointRadius, index, this.options.elements.point.radius), - backgroundColor: point.custom && point.custom.backgroundColor ? point.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBackgroundColor, index, this.options.elements.point.backgroundColor), - borderColor: point.custom && point.custom.borderColor ? point.custom.borderColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBorderColor, index, this.options.elements.point.borderColor), - borderWidth: point.custom && point.custom.borderWidth ? point.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBorderWidth, index, this.options.elements.point.borderWidth), - skip: this.data.datasets[datasetIndex].data[index] === null, - - // Tooltip - hitRadius: point.custom && point.custom.hitRadius ? point.custom.hitRadius : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].hitRadius, index, this.options.elements.point.hitRadius), - }, - }); - }, this); - - - // Update control points for the bezier curve - this.eachElement(function(point, index, dataset, datasetIndex) { - var controlPoints = helpers.splineCurve( - this.previousPoint(dataset, index)._model, - point._model, - this.nextPoint(dataset, index)._model, - point._model.tension - ); - - point._model.controlPointPreviousX = controlPoints.previous.x; - point._model.controlPointNextX = controlPoints.next.x; - - // Prevent the bezier going outside of the bounds of the graph - - // Cap puter bezier handles to the upper/lower scale bounds - if (controlPoints.next.y > this.chartArea.bottom) { - point._model.controlPointNextY = this.chartArea.bottom; - } else if (controlPoints.next.y < this.chartArea.top) { - point._model.controlPointNextY = this.chartArea.top; - } else { - point._model.controlPointNextY = controlPoints.next.y; - } - - // Cap inner bezier handles to the upper/lower scale bounds - if (controlPoints.previous.y > this.chartArea.bottom) { - point._model.controlPointPreviousY = this.chartArea.bottom; - } else if (controlPoints.previous.y < this.chartArea.top) { - point._model.controlPointPreviousY = this.chartArea.top; - } else { - point._model.controlPointPreviousY = controlPoints.previous.y; - } - - // Now pivot the point for animation - point.pivot(); - }, this); - - this.render(animationDuration); - }, - buildScale: function() { - var self = this; - - var ScaleConstructor = Chart.scales.getScaleConstructor(this.options.scale.type); - this.scale = new ScaleConstructor({ - options: this.options.scale, - height: this.chart.height, - width: this.chart.width, - xCenter: this.chart.width / 2, - yCenter: this.chart.height / 2, - ctx: this.chart.ctx, - labels: this.data.labels, - valuesCount: this.data.datasets[0].data.length, - calculateRange: function() { - this.min = null; - this.max = null; - - helpers.each(self.data.datasets, function(dataset) { - if (dataset.yAxisID === this.id) { - helpers.each(dataset.data, function(value, index) { - if (this.min === null) { - this.min = value; - } else if (value < this.min) { - this.min = value; - } - - if (this.max === null) { - this.max = value; - } else if (value > this.max) { - this.max = value; - } - }, this); - } - }, this); - } - }); - - this.scale.setScaleSize(); - this.scale.calculateRange(); - this.scale.generateTicks(); - this.scale.buildYLabels(); - }, - draw: function(ease) { - var easingDecimal = ease || 1; - this.clear(); - - // Draw all the scales - this.scale.draw(this.chartArea); - - // reverse for-loop for proper stacking - for (var i = this.data.datasets.length - 1; i >= 0; i--) { - - var dataset = this.data.datasets[i]; - - // Transition Point Locations - helpers.each(dataset.metaData, function(point, index) { - point.transition(easingDecimal); - }, this); - - // Transition and Draw the line - dataset.metaDataset.transition(easingDecimal).draw(); - - // Draw the points - helpers.each(dataset.metaData, function(point) { - point.draw(); - }); - } - - // Finally draw the tooltip - this.tooltip.transition(easingDecimal).draw(); - }, - events: function(e) { - - this.lastActive = this.lastActive || []; - - // Find Active Elements - // If exiting chart - if (e.type == 'mouseout') { - this.active = []; - } else { - this.active = function() { - switch (this.options.hover.mode) { - case 'single': - return this.getElementAtEvent(e); - case 'label': - return this.getElementsAtEvent(e); - case 'dataset': - return this.getDatasetAtEvent(e); - default: - return e; - } - }.call(this); - } - - // On Hover hook - if (this.options.hover.onHover) { - this.options.hover.onHover.call(this, this.active); - } - - if (e.type == 'mouseup' || e.type == 'click') { - if (this.options.onClick) { - this.options.onClick.call(this, e, this.active); - } - } - - var dataset; - var index; - // Remove styling for last active (even if it may still be active) - if (this.lastActive.length) { - switch (this.options.hover.mode) { - case 'single': - dataset = this.data.datasets[this.lastActive[0]._datasetIndex]; - index = this.lastActive[0]._index; - - this.lastActive[0]._model.radius = this.lastActive[0].custom && this.lastActive[0].custom.radius ? this.lastActive[0].custom.pointRadius : helpers.getValueAtIndexOrDefault(dataset.pointRadius, index, this.options.elements.point.radius); - this.lastActive[0]._model.backgroundColor = this.lastActive[0].custom && this.lastActive[0].custom.backgroundColor ? this.lastActive[0].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointBackgroundColor, index, this.options.elements.point.backgroundColor); - this.lastActive[0]._model.borderColor = this.lastActive[0].custom && this.lastActive[0].custom.borderColor ? this.lastActive[0].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.pointBorderColor, index, this.options.elements.point.borderColor); - this.lastActive[0]._model.borderWidth = this.lastActive[0].custom && this.lastActive[0].custom.borderWidth ? this.lastActive[0].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.options.elements.point.borderWidth); - break; - case 'label': - for (var i = 0; i < this.lastActive.length; i++) { - dataset = this.data.datasets[this.lastActive[i]._datasetIndex]; - index = this.lastActive[i]._index; - - this.lastActive[i]._model.radius = this.lastActive[i].custom && this.lastActive[i].custom.radius ? this.lastActive[i].custom.pointRadius : helpers.getValueAtIndexOrDefault(dataset.pointRadius, index, this.options.elements.point.radius); - this.lastActive[i]._model.backgroundColor = this.lastActive[i].custom && this.lastActive[i].custom.backgroundColor ? this.lastActive[i].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointBackgroundColor, index, this.options.elements.point.backgroundColor); - this.lastActive[i]._model.borderColor = this.lastActive[i].custom && this.lastActive[i].custom.borderColor ? this.lastActive[i].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.pointBorderColor, index, this.options.elements.point.borderColor); - this.lastActive[i]._model.borderWidth = this.lastActive[i].custom && this.lastActive[i].custom.borderWidth ? this.lastActive[i].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.options.elements.point.borderWidth); - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - // Built in hover styling - if (this.active.length && this.options.hover.mode) { - switch (this.options.hover.mode) { - case 'single': - dataset = this.data.datasets[this.active[0]._datasetIndex]; - index = this.active[0]._index; - - this.active[0]._model.radius = this.active[0].custom && this.active[0].custom.hoverRadius ? this.active[0].custom.hoverRadius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.active[0]._model.radius + 2); - this.active[0]._model.backgroundColor = this.active[0].custom && this.active[0].custom.hoverBackgroundColor ? this.active[0].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(this.active[0]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - this.active[0]._model.borderColor = this.active[0].custom && this.active[0].custom.hoverBorderColor ? this.active[0].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(this.active[0]._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - this.active[0]._model.borderWidth = this.active[0].custom && this.active[0].custom.hoverBorderWidth ? this.active[0].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.active[0]._model.borderWidth + 2); - break; - case 'label': - for (var i = 0; i < this.active.length; i++) { - dataset = this.data.datasets[this.active[i]._datasetIndex]; - index = this.active[i]._index; - - this.active[i]._model.radius = this.active[i].custom && this.active[i].custom.hoverRadius ? this.active[i].custom.hoverRadius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.active[i]._model.radius + 2); - this.active[i]._model.backgroundColor = this.active[i].custom && this.active[i].custom.hoverBackgroundColor ? this.active[i].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(this.active[i]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - this.active[i]._model.borderColor = this.active[i].custom && this.active[i].custom.hoverBorderColor ? this.active[i].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(this.active[i]._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - this.active[i]._model.borderWidth = this.active[i].custom && this.active[i].custom.hoverBorderWidth ? this.active[i].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.active[i]._model.borderWidth + 2); - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - // Built in Tooltips - if (this.options.tooltips.enabled) { - - // The usual updates - this.tooltip.initialize(); - - // Active - if (this.active.length) { - this.tooltip._model.opacity = 1; - - helpers.extend(this.tooltip, { - _active: this.active, - }); - - this.tooltip.update(); - } else { - // Inactive - this.tooltip._model.opacity = 0; - } - } - - // Hover animations - this.tooltip.pivot(); - - if (!this.animating) { - var changed; - - helpers.each(this.active, function(element, index) { - if (element !== this.lastActive[index]) { - changed = true; - } - }, this); - - // If entering, leaving, or changing elements, animate the change via pivot - if ((!this.lastActive.length && this.active.length) || - (this.lastActive.length && !this.active.length) || - (this.lastActive.length && this.active.length && changed)) { - - this.stop(); - this.render(this.options.hover.animationDuration); - } - } - - // Remember Last Active - this.lastActive = this.active; - return this; - }, - }); + // label settings + labels: { + show: true, + template: "<%=value.toLocaleString()%>", + fontSize: 12, + fontStyle: "normal", + fontColor: "#666", + fontFamily: "Helvetica Neue", + + //Boolean - Show a backdrop to the scale label + showLabelBackdrop: true, + + //String - The colour of the label backdrop + backdropColor: "rgba(255,255,255,0.75)", + + //Number - The backdrop padding above & below the label in pixels + backdropPaddingY: 2, + + //Number - The backdrop padding to the side of the label in pixels + backdropPaddingX: 2, + }, + + pointLabels: { + //String - Point label font declaration + fontFamily: "'Arial'", + + //String - Point label font weight + fontStyle: "normal", + + //Number - Point label font size in pixels + fontSize: 10, + + //String - Point label font colour + fontColor: "#666", + }, + }, + + elements: { + line: { + tension: 0, // no bezier in radar + } + }, + + //String - A legend template + legendTemplate: "" + + }, + + initialize: function() { + + // Events + helpers.bindEvents(this, this.options.events, this.events); + + // Create a new line and its points for each dataset and piece of data + helpers.each(this.data.datasets, function(dataset, datasetIndex) { + + dataset.metaDataset = new Chart.Line({ + _chart: this.chart, + _datasetIndex: datasetIndex, + _points: dataset.metaData, + _loop: true + }); + + dataset.metaData = []; + + helpers.each(dataset.data, function(dataPoint, index) { + dataset.metaData.push(new Chart.Point({ + _datasetIndex: datasetIndex, + _index: index, + _chart: this.chart, + _model: { + x: 0, //xScale.getPixelForValue(null, index, true), + y: 0, //this.chartArea.bottom, + }, + })); + + }, this); + }, this); + + // Build the scale. + this.buildScale(); + + // Create tooltip instance exclusively for this chart with some defaults. + this.tooltip = new Chart.Tooltip({ + _chart: this.chart, + _data: this.data, + _options: this.options, + }, this); + + // Need to fit scales before we reset elements. + Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); + + // Reset so that we animation from the baseline + this.resetElements(); + + // Update that shiz + this.update(); + }, + nextPoint: function(collection, index) { + return collection[index + 1] || collection[0]; + }, + previousPoint: function(collection, index) { + return collection[index - 1] || collection[collection.length - 1]; + }, + resetElements: function() { + + // Update the points + this.eachElement(function(point, index, dataset, datasetIndex) { + helpers.extend(point, { + // Utility + _chart: this.chart, + _datasetIndex: datasetIndex, + _index: index, + _scale: this.scale, + + // Desired view properties + _model: { + x: this.scale.xCenter, + y: this.scale.yCenter, + + // Appearance + tension: point.custom && point.custom.tension ? point.custom.tension : this.options.elements.line.tension, + radius: point.custom && point.custom.radius ? point.custom.pointRadius : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointRadius, index, this.options.elements.point.radius), + backgroundColor: point.custom && point.custom.backgroundColor ? point.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBackgroundColor, index, this.options.elements.point.backgroundColor), + borderColor: point.custom && point.custom.borderColor ? point.custom.borderColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBorderColor, index, this.options.elements.point.borderColor), + borderWidth: point.custom && point.custom.borderWidth ? point.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBorderWidth, index, this.options.elements.point.borderWidth), + skip: this.data.datasets[datasetIndex].data[index] === null, + + // Tooltip + hitRadius: point.custom && point.custom.hitRadius ? point.custom.hitRadius : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].hitRadius, index, this.options.elements.point.hitRadius), + }, + }); + }, this); + + // Update control points for the bezier curve + this.eachElement(function(point, index, dataset, datasetIndex) { + var controlPoints = helpers.splineCurve( + this.previousPoint(dataset, index)._model, + point._model, + this.nextPoint(dataset, index)._model, + point._model.tension + ); + + point._model.controlPointPreviousX = this.scale.xCenter; + point._model.controlPointPreviousY = this.scale.yCenter; + point._model.controlPointNextX = this.scale.xCenter; + point._model.controlPointNextY = this.scale.yCenter; + + // Now pivot the point for animation + point.pivot(); + }, this); + }, + update: function(animationDuration) { + Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); + + // Update the lines + this.eachDataset(function(dataset, datasetIndex) { + var scaleBase; + + if (this.scale.min < 0 && this.scale.max < 0) { + scaleBase = this.scale.getPointPosition(0, this.scale.max); + } else if (this.scale.min > 0 && this.scale.max > 0) { + scaleBase = this.scale.getPointPosition(0, this.scale.min); + } else { + scaleBase = this.scale.getPointPosition(0, 0); + } + + helpers.extend(dataset.metaDataset, { + // Utility + _datasetIndex: datasetIndex, + + // Data + _children: dataset.metaData, + + // Model + _model: { + // Appearance + tension: dataset.tension || this.options.elements.line.tension, + backgroundColor: dataset.backgroundColor || this.options.elements.line.backgroundColor, + borderWidth: dataset.borderWidth || this.options.elements.line.borderWidth, + borderColor: dataset.borderColor || this.options.elements.line.borderColor, + fill: dataset.fill !== undefined ? dataset.fill : this.options.elements.line.fill, // use the value from the dataset if it was provided. else fall back to the default + skipNull: dataset.skipNull !== undefined ? dataset.skipNull : this.options.elements.line.skipNull, + drawNull: dataset.drawNull !== undefined ? dataset.drawNull : this.options.elements.line.drawNull, + + // Scale + scaleTop: this.scale.top, + scaleBottom: this.scale.bottom, + scaleZero: scaleBase, + }, + }); + + dataset.metaDataset.pivot(); + }); + + // Update the points + this.eachElement(function(point, index, dataset, datasetIndex) { + var pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(this.data.datasets[datasetIndex].data[index])); + + helpers.extend(point, { + // Utility + _chart: this.chart, + _datasetIndex: datasetIndex, + _index: index, + + // Desired view properties + _model: { + x: pointPosition.x, // value not used in dataset scale, but we want a consistent API between scales + y: pointPosition.y, + + // Appearance + tension: point.custom && point.custom.tension ? point.custom.tension : this.options.elements.line.tension, + radius: point.custom && point.custom.radius ? point.custom.pointRadius : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointRadius, index, this.options.elements.point.radius), + backgroundColor: point.custom && point.custom.backgroundColor ? point.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBackgroundColor, index, this.options.elements.point.backgroundColor), + borderColor: point.custom && point.custom.borderColor ? point.custom.borderColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBorderColor, index, this.options.elements.point.borderColor), + borderWidth: point.custom && point.custom.borderWidth ? point.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBorderWidth, index, this.options.elements.point.borderWidth), + skip: this.data.datasets[datasetIndex].data[index] === null, + + // Tooltip + hitRadius: point.custom && point.custom.hitRadius ? point.custom.hitRadius : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].hitRadius, index, this.options.elements.point.hitRadius), + }, + }); + }, this); + + + // Update control points for the bezier curve + this.eachElement(function(point, index, dataset, datasetIndex) { + var controlPoints = helpers.splineCurve( + this.previousPoint(dataset, index)._model, + point._model, + this.nextPoint(dataset, index)._model, + point._model.tension + ); + + point._model.controlPointPreviousX = controlPoints.previous.x; + point._model.controlPointNextX = controlPoints.next.x; + + // Prevent the bezier going outside of the bounds of the graph + + // Cap puter bezier handles to the upper/lower scale bounds + if (controlPoints.next.y > this.chartArea.bottom) { + point._model.controlPointNextY = this.chartArea.bottom; + } else if (controlPoints.next.y < this.chartArea.top) { + point._model.controlPointNextY = this.chartArea.top; + } else { + point._model.controlPointNextY = controlPoints.next.y; + } + + // Cap inner bezier handles to the upper/lower scale bounds + if (controlPoints.previous.y > this.chartArea.bottom) { + point._model.controlPointPreviousY = this.chartArea.bottom; + } else if (controlPoints.previous.y < this.chartArea.top) { + point._model.controlPointPreviousY = this.chartArea.top; + } else { + point._model.controlPointPreviousY = controlPoints.previous.y; + } + + // Now pivot the point for animation + point.pivot(); + }, this); + + this.render(animationDuration); + }, + buildScale: function() { + var self = this; + + var ScaleConstructor = Chart.scales.getScaleConstructor(this.options.scale.type); + this.scale = new ScaleConstructor({ + options: this.options.scale, + height: this.chart.height, + width: this.chart.width, + xCenter: this.chart.width / 2, + yCenter: this.chart.height / 2, + ctx: this.chart.ctx, + labels: this.data.labels, + valuesCount: this.data.datasets[0].data.length, + data: this.data, + }); + + this.scale.setScaleSize(); + this.scale.calculateRange(); + this.scale.generateTicks(); + this.scale.buildYLabels(); + }, + draw: function(ease) { + var easingDecimal = ease || 1; + this.clear(); + + // Draw all the scales + this.scale.draw(this.chartArea); + + // reverse for-loop for proper stacking + for (var i = this.data.datasets.length - 1; i >= 0; i--) { + + var dataset = this.data.datasets[i]; + + // Transition Point Locations + helpers.each(dataset.metaData, function(point, index) { + point.transition(easingDecimal); + }, this); + + // Transition and Draw the line + dataset.metaDataset.transition(easingDecimal).draw(); + + // Draw the points + helpers.each(dataset.metaData, function(point) { + point.draw(); + }); + } + + // Finally draw the tooltip + this.tooltip.transition(easingDecimal).draw(); + }, + events: function(e) { + + this.lastActive = this.lastActive || []; + + // Find Active Elements + // If exiting chart + if (e.type == 'mouseout') { + this.active = []; + } else { + this.active = function() { + switch (this.options.hover.mode) { + case 'single': + return this.getElementAtEvent(e); + case 'label': + return this.getElementsAtEvent(e); + case 'dataset': + return this.getDatasetAtEvent(e); + default: + return e; + } + }.call(this); + } + + // On Hover hook + if (this.options.hover.onHover) { + this.options.hover.onHover.call(this, this.active); + } + + if (e.type == 'mouseup' || e.type == 'click') { + if (this.options.onClick) { + this.options.onClick.call(this, e, this.active); + } + } + + var dataset; + var index; + // Remove styling for last active (even if it may still be active) + if (this.lastActive.length) { + switch (this.options.hover.mode) { + case 'single': + dataset = this.data.datasets[this.lastActive[0]._datasetIndex]; + index = this.lastActive[0]._index; + + this.lastActive[0]._model.radius = this.lastActive[0].custom && this.lastActive[0].custom.radius ? this.lastActive[0].custom.pointRadius : helpers.getValueAtIndexOrDefault(dataset.pointRadius, index, this.options.elements.point.radius); + this.lastActive[0]._model.backgroundColor = this.lastActive[0].custom && this.lastActive[0].custom.backgroundColor ? this.lastActive[0].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointBackgroundColor, index, this.options.elements.point.backgroundColor); + this.lastActive[0]._model.borderColor = this.lastActive[0].custom && this.lastActive[0].custom.borderColor ? this.lastActive[0].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.pointBorderColor, index, this.options.elements.point.borderColor); + this.lastActive[0]._model.borderWidth = this.lastActive[0].custom && this.lastActive[0].custom.borderWidth ? this.lastActive[0].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.options.elements.point.borderWidth); + break; + case 'label': + for (var i = 0; i < this.lastActive.length; i++) { + dataset = this.data.datasets[this.lastActive[i]._datasetIndex]; + index = this.lastActive[i]._index; + + this.lastActive[i]._model.radius = this.lastActive[i].custom && this.lastActive[i].custom.radius ? this.lastActive[i].custom.pointRadius : helpers.getValueAtIndexOrDefault(dataset.pointRadius, index, this.options.elements.point.radius); + this.lastActive[i]._model.backgroundColor = this.lastActive[i].custom && this.lastActive[i].custom.backgroundColor ? this.lastActive[i].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointBackgroundColor, index, this.options.elements.point.backgroundColor); + this.lastActive[i]._model.borderColor = this.lastActive[i].custom && this.lastActive[i].custom.borderColor ? this.lastActive[i].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.pointBorderColor, index, this.options.elements.point.borderColor); + this.lastActive[i]._model.borderWidth = this.lastActive[i].custom && this.lastActive[i].custom.borderWidth ? this.lastActive[i].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.options.elements.point.borderWidth); + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + // Built in hover styling + if (this.active.length && this.options.hover.mode) { + switch (this.options.hover.mode) { + case 'single': + dataset = this.data.datasets[this.active[0]._datasetIndex]; + index = this.active[0]._index; + + this.active[0]._model.radius = this.active[0].custom && this.active[0].custom.hoverRadius ? this.active[0].custom.hoverRadius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.active[0]._model.radius + 2); + this.active[0]._model.backgroundColor = this.active[0].custom && this.active[0].custom.hoverBackgroundColor ? this.active[0].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(this.active[0]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); + this.active[0]._model.borderColor = this.active[0].custom && this.active[0].custom.hoverBorderColor ? this.active[0].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(this.active[0]._model.borderColor).saturate(0.5).darken(0.1).rgbString()); + this.active[0]._model.borderWidth = this.active[0].custom && this.active[0].custom.hoverBorderWidth ? this.active[0].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.active[0]._model.borderWidth + 2); + break; + case 'label': + for (var i = 0; i < this.active.length; i++) { + dataset = this.data.datasets[this.active[i]._datasetIndex]; + index = this.active[i]._index; + + this.active[i]._model.radius = this.active[i].custom && this.active[i].custom.hoverRadius ? this.active[i].custom.hoverRadius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.active[i]._model.radius + 2); + this.active[i]._model.backgroundColor = this.active[i].custom && this.active[i].custom.hoverBackgroundColor ? this.active[i].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(this.active[i]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); + this.active[i]._model.borderColor = this.active[i].custom && this.active[i].custom.hoverBorderColor ? this.active[i].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(this.active[i]._model.borderColor).saturate(0.5).darken(0.1).rgbString()); + this.active[i]._model.borderWidth = this.active[i].custom && this.active[i].custom.hoverBorderWidth ? this.active[i].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.active[i]._model.borderWidth + 2); + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + // Built in Tooltips + if (this.options.tooltips.enabled) { + + // The usual updates + this.tooltip.initialize(); + + // Active + if (this.active.length) { + this.tooltip._model.opacity = 1; + + helpers.extend(this.tooltip, { + _active: this.active, + }); + + this.tooltip.update(); + } else { + // Inactive + this.tooltip._model.opacity = 0; + } + } + + // Hover animations + this.tooltip.pivot(); + + if (!this.animating) { + var changed; + + helpers.each(this.active, function(element, index) { + if (element !== this.lastActive[index]) { + changed = true; + } + }, this); + + // If entering, leaving, or changing elements, animate the change via pivot + if ((!this.lastActive.length && this.active.length) || + (this.lastActive.length && !this.active.length) || + (this.lastActive.length && this.active.length && changed)) { + + this.stop(); + this.render(this.options.hover.animationDuration); + } + } + + // Remember Last Active + this.lastActive = this.active; + return this; + }, + }); }).call(this); diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 2372e270e..7bcda29c9 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -1,361 +1,380 @@ (function() { - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - var LinearRadialScale = Chart.Element.extend({ - initialize: function() { - this.size = helpers.min([this.height, this.width]); - this.drawingArea = (this.options.display) ? (this.size / 2) - (this.options.labels.fontSize / 2 + this.options.labels.backdropPaddingY) : (this.size / 2); - }, - calculateCenterOffset: function(value) { - // Take into account half font size + the yPadding of the top value - var scalingFactor = this.drawingArea / (this.max - this.min); - return (value - this.min) * scalingFactor; - }, - update: function() { - if (!this.options.lineArc) { - this.setScaleSize(); - } else { - this.drawingArea = (this.options.display) ? (this.size / 2) - (this.fontSize / 2 + this.backdropPaddingY) : (this.size / 2); - } - - this.buildYLabels(); - }, - calculateRange: helpers.noop, // overridden in chart - generateTicks: function() { - // We need to decide how many ticks we are going to have. Each tick draws a grid line. - // There are two possibilities. The first is that the user has manually overridden the scale - // calculations in which case the job is easy. The other case is that we have to do it ourselves - // - // We assume at this point that the scale object has been updated with the following values - // by the chart. - // min: this is the minimum value of the scale - // max: this is the maximum value of the scale - // options: contains the options for the scale. This is referenced from the user settings - // rather than being cloned. This ensures that updates always propogate to a redraw - - // Reset the ticks array. Later on, we will draw a grid line at these positions - // The array simply contains the numerical value of the spots where ticks will be - this.ticks = []; - - if (this.options.override) { - // The user has specified the manual override. We use <= instead of < so that - // we get the final line - for (var i = 0; i <= this.options.override.steps; ++i) { - var value = this.options.override.start + (i * this.options.override.stepWidth); - ticks.push(value); - } - } else { - // Figure out what the max number of ticks we can support it is based on the size of - // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 - // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on - // the graph - - var maxTicks = Math.min(11, Math.ceil(this.drawingArea / (2 * this.options.labels.fontSize))); - - // Make sure we always have at least 2 ticks - maxTicks = Math.max(2, maxTicks); - - // To get a "nice" value for the tick spacing, we will use the appropriately named - // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks - // for details. - - // If we are forcing it to begin at 0, but 0 will already be rendered on the chart, - // do nothing since that would make the chart weird. If the user really wants a weird chart - // axis, they can manually override it - if (this.options.beginAtZero) { - var minSign = helpers.sign(this.min); - var maxSign = helpers.sign(this.max); - - if (minSign < 0 && maxSign < 0) { - // move the top up to 0 - this.max = 0; - } else if (minSign > 0 && maxSign > 0) { - // move the botttom down to 0 - this.min = 0; - } - } - - var niceRange = helpers.niceNum(this.max - this.min, false); - var spacing = helpers.niceNum(niceRange / (maxTicks - 1), true); - var niceMin = Math.floor(this.min / spacing) * spacing; - var niceMax = Math.ceil(this.max / spacing) * spacing; - - // Put the values into the ticks array - for (var j = niceMin; j <= niceMax; j += spacing) { - this.ticks.push(j); - } - } - - if (this.options.position == "left" || this.options.position == "right") { - // We are in a vertical orientation. The top value is the highest. So reverse the array - this.ticks.reverse(); - } - - // At this point, we need to update our max and min given the tick values since we have expanded the - // range of the scale - this.max = helpers.max(this.ticks); - this.min = helpers.min(this.ticks); - }, - buildYLabels: function() { - this.yLabels = []; - - helpers.each(this.ticks, function(tick, index, ticks) { - var label; - - if (this.options.labels.userCallback) { - // If the user provided a callback for label generation, use that as first priority - label = this.options.labels.userCallback(tick, index, ticks); - } else if (this.options.labels.template) { - // else fall back to the template string - label = helpers.template(this.options.labels.template, { - value: tick - }); - } - - this.yLabels.push(label ? label : ""); - }, this); - }, - getCircumference: function() { - return ((Math.PI * 2) / this.valuesCount); - }, - setScaleSize: function() { - /* - * Right, this is really confusing and there is a lot of maths going on here - * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 - * - * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif - * - * Solution: - * - * We assume the radius of the polygon is half the size of the canvas at first - * at each index we check if the text overlaps. - * - * Where it does, we store that angle and that index. - * - * After finding the largest index and angle we calculate how much we need to remove - * from the shape radius to move the point inwards by that x. - * - * We average the left and right distances to get the maximum shape radius that can fit in the box - * along with labels. - * - * Once we have that, we can find the centre point for the chart, by taking the x text protrusion - * on each side, removing that from the size, halving it and adding the left x protrusion width. - * - * This will mean we have a shape fitted to the canvas, as large as it can be with the labels - * and position it in the most space efficient manner - * - * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif - */ - - - // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. - // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points - var largestPossibleRadius = helpers.min([(this.height / 2 - this.options.pointLabels.fontSize - 5), this.width / 2]), - pointPosition, - i, - textWidth, - halfTextWidth, - furthestRight = this.width, - furthestRightIndex, - furthestRightAngle, - furthestLeft = 0, - furthestLeftIndex, - furthestLeftAngle, - xProtrusionLeft, - xProtrusionRight, - radiusReductionRight, - radiusReductionLeft, - maxWidthRadius; - this.ctx.font = helpers.fontString(this.options.pointLabels.fontSize, this.options.pointLabels.fontStyle, this.options.pointLabels.fontFamily); - for (i = 0; i < this.valuesCount; i++) { - // 5px to space the text slightly out - similar to what we do in the draw function. - pointPosition = this.getPointPosition(i, largestPossibleRadius); - textWidth = this.ctx.measureText(helpers.template(this.options.labels.template, { - value: this.labels[i] - })).width + 5; - if (i === 0 || i === this.valuesCount / 2) { - // If we're at index zero, or exactly the middle, we're at exactly the top/bottom - // of the radar chart, so text will be aligned centrally, so we'll half it and compare - // w/left and right text sizes - halfTextWidth = textWidth / 2; - if (pointPosition.x + halfTextWidth > furthestRight) { - furthestRight = pointPosition.x + halfTextWidth; - furthestRightIndex = i; - } - if (pointPosition.x - halfTextWidth < furthestLeft) { - furthestLeft = pointPosition.x - halfTextWidth; - furthestLeftIndex = i; - } - } else if (i < this.valuesCount / 2) { - // Less than half the values means we'll left align the text - if (pointPosition.x + textWidth > furthestRight) { - furthestRight = pointPosition.x + textWidth; - furthestRightIndex = i; - } - } else if (i > this.valuesCount / 2) { - // More than half the values means we'll right align the text - if (pointPosition.x - textWidth < furthestLeft) { - furthestLeft = pointPosition.x - textWidth; - furthestLeftIndex = i; - } - } - } - - xProtrusionLeft = furthestLeft; - - xProtrusionRight = Math.ceil(furthestRight - this.width); - - furthestRightAngle = this.getIndexAngle(furthestRightIndex); - - furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); - - radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI / 2); - - radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI / 2); - - // Ensure we actually need to reduce the size of the chart - radiusReductionRight = (helpers.isNumber(radiusReductionRight)) ? radiusReductionRight : 0; - radiusReductionLeft = (helpers.isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; - - this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2; - - //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) - this.setCenterPoint(radiusReductionLeft, radiusReductionRight); - - }, - setCenterPoint: function(leftMovement, rightMovement) { - - var maxRight = this.width - rightMovement - this.drawingArea, - maxLeft = leftMovement + this.drawingArea; - - this.xCenter = (maxLeft + maxRight) / 2; - // Always vertically in the centre as the text height doesn't change - this.yCenter = (this.height / 2); - }, - - getIndexAngle: function(index) { - var angleMultiplier = (Math.PI * 2) / this.valuesCount; - // Start from the top instead of right, so remove a quarter of the circle - - return index * angleMultiplier - (Math.PI / 2); - }, - getPointPosition: function(index, distanceFromCenter) { - var thisAngle = this.getIndexAngle(index); - return { - x: (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, - y: (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter - }; - }, - draw: function() { - if (this.options.display) { - var ctx = this.ctx; - helpers.each(this.yLabels, function(label, index) { - // Don't draw a centre value - if (index > 0) { - var yCenterOffset = index * (this.drawingArea / Math.max(this.ticks.length, 1)), - yHeight = this.yCenter - yCenterOffset, - pointPosition; - - // Draw circular lines around the scale - if (this.options.gridLines.show) { - ctx.strokeStyle = this.options.gridLines.color; - ctx.lineWidth = this.options.gridLines.lineWidth; - - if (this.options.lineArc) { - ctx.beginPath(); - ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI * 2); - ctx.closePath(); - ctx.stroke(); - } else { - ctx.beginPath(); - for (var i = 0; i < this.valuesCount; i++) { - pointPosition = this.getPointPosition(i, this.calculateCenterOffset(this.ticks[index])); - if (i === 0) { - ctx.moveTo(pointPosition.x, pointPosition.y); - } else { - ctx.lineTo(pointPosition.x, pointPosition.y); - } - } - ctx.closePath(); - ctx.stroke(); - } - } - - if (this.options.labels.show) { - ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); - - if (this.showLabelBackdrop) { - var labelWidth = ctx.measureText(label).width; - ctx.fillStyle = this.options.labels.backdropColor; - ctx.fillRect( - this.xCenter - labelWidth / 2 - this.options.labels.backdropPaddingX, - yHeight - this.fontSize / 2 - this.options.labels.backdropPaddingY, - labelWidth + this.options.labels.backdropPaddingX * 2, - this.options.labels.fontSize + this.options.lables.backdropPaddingY * 2 - ); - } - - ctx.textAlign = 'center'; - ctx.textBaseline = "middle"; - ctx.fillStyle = this.options.labels.fontColor; - ctx.fillText(label, this.xCenter, yHeight); - } - } - }, this); - - if (!this.options.lineArc) { - ctx.lineWidth = this.options.angleLines.lineWidth; - ctx.strokeStyle = this.options.angleLines.color; - - for (var i = this.valuesCount - 1; i >= 0; i--) { - if (this.options.angleLines.show) { - var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); - ctx.beginPath(); - ctx.moveTo(this.xCenter, this.yCenter); - ctx.lineTo(outerPosition.x, outerPosition.y); - ctx.stroke(); - ctx.closePath(); - } - // Extra 3px out for some label spacing - var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); - ctx.font = helpers.fontString(this.options.pointLabels.fontSize, this.options.pointLabels.fontStyle, this.options.pointLabels.fontFamily); - ctx.fillStyle = this.options.pointLabels.fontColor; - - var labelsCount = this.labels.length, - halfLabelsCount = this.labels.length / 2, - quarterLabelsCount = halfLabelsCount / 2, - upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), - exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); - if (i === 0) { - ctx.textAlign = 'center'; - } else if (i === halfLabelsCount) { - ctx.textAlign = 'center'; - } else if (i < halfLabelsCount) { - ctx.textAlign = 'left'; - } else { - ctx.textAlign = 'right'; - } - - // Set the correct text baseline based on outer positioning - if (exactQuarter) { - ctx.textBaseline = 'middle'; - } else if (upperHalf) { - ctx.textBaseline = 'bottom'; - } else { - ctx.textBaseline = 'top'; - } - - ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); - } - } - } - } - }); - Chart.scales.registerScaleType("radialLinear", LinearRadialScale); + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var LinearRadialScale = Chart.Element.extend({ + initialize: function() { + this.size = helpers.min([this.height, this.width]); + this.drawingArea = (this.options.display) ? (this.size / 2) - (this.options.labels.fontSize / 2 + this.options.labels.backdropPaddingY) : (this.size / 2); + }, + calculateCenterOffset: function(value) { + // Take into account half font size + the yPadding of the top value + var scalingFactor = this.drawingArea / (this.max - this.min); + return (value - this.min) * scalingFactor; + }, + update: function() { + if (!this.options.lineArc) { + this.setScaleSize(); + } else { + this.drawingArea = (this.options.display) ? (this.size / 2) - (this.fontSize / 2 + this.backdropPaddingY) : (this.size / 2); + } + + this.buildYLabels(); + }, + calculateRange: function() { + this.min = null; + this.max = null; + + helpers.each(this.data.datasets, function(dataset) { + helpers.each(dataset.data, function(value, index) { + if (this.min === null) { + this.min = value; + } else if (value < this.min) { + this.min = value; + } + + if (this.max === null) { + this.max = value; + } else if (value > this.max) { + this.max = value; + } + }, this); + }, this); + }, + generateTicks: function() { + // We need to decide how many ticks we are going to have. Each tick draws a grid line. + // There are two possibilities. The first is that the user has manually overridden the scale + // calculations in which case the job is easy. The other case is that we have to do it ourselves + // + // We assume at this point that the scale object has been updated with the following values + // by the chart. + // min: this is the minimum value of the scale + // max: this is the maximum value of the scale + // options: contains the options for the scale. This is referenced from the user settings + // rather than being cloned. This ensures that updates always propogate to a redraw + + // Reset the ticks array. Later on, we will draw a grid line at these positions + // The array simply contains the numerical value of the spots where ticks will be + this.ticks = []; + + if (this.options.override) { + // The user has specified the manual override. We use <= instead of < so that + // we get the final line + for (var i = 0; i <= this.options.override.steps; ++i) { + var value = this.options.override.start + (i * this.options.override.stepWidth); + ticks.push(value); + } + } else { + // Figure out what the max number of ticks we can support it is based on the size of + // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 + // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on + // the graph + + var maxTicks = Math.min(11, Math.ceil(this.drawingArea / (2 * this.options.labels.fontSize))); + + // Make sure we always have at least 2 ticks + maxTicks = Math.max(2, maxTicks); + + // To get a "nice" value for the tick spacing, we will use the appropriately named + // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks + // for details. + + // If we are forcing it to begin at 0, but 0 will already be rendered on the chart, + // do nothing since that would make the chart weird. If the user really wants a weird chart + // axis, they can manually override it + if (this.options.beginAtZero) { + var minSign = helpers.sign(this.min); + var maxSign = helpers.sign(this.max); + + if (minSign < 0 && maxSign < 0) { + // move the top up to 0 + this.max = 0; + } else if (minSign > 0 && maxSign > 0) { + // move the botttom down to 0 + this.min = 0; + } + } + + var niceRange = helpers.niceNum(this.max - this.min, false); + var spacing = helpers.niceNum(niceRange / (maxTicks - 1), true); + var niceMin = Math.floor(this.min / spacing) * spacing; + var niceMax = Math.ceil(this.max / spacing) * spacing; + + // Put the values into the ticks array + for (var j = niceMin; j <= niceMax; j += spacing) { + this.ticks.push(j); + } + } + + if (this.options.position == "left" || this.options.position == "right") { + // We are in a vertical orientation. The top value is the highest. So reverse the array + this.ticks.reverse(); + } + + // At this point, we need to update our max and min given the tick values since we have expanded the + // range of the scale + this.max = helpers.max(this.ticks); + this.min = helpers.min(this.ticks); + }, + buildYLabels: function() { + this.yLabels = []; + + helpers.each(this.ticks, function(tick, index, ticks) { + var label; + + if (this.options.labels.userCallback) { + // If the user provided a callback for label generation, use that as first priority + label = this.options.labels.userCallback(tick, index, ticks); + } else if (this.options.labels.template) { + // else fall back to the template string + label = helpers.template(this.options.labels.template, { + value: tick + }); + } + + this.yLabels.push(label ? label : ""); + }, this); + }, + getCircumference: function() { + return ((Math.PI * 2) / this.valuesCount); + }, + setScaleSize: function() { + /* + * Right, this is really confusing and there is a lot of maths going on here + * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 + * + * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif + * + * Solution: + * + * We assume the radius of the polygon is half the size of the canvas at first + * at each index we check if the text overlaps. + * + * Where it does, we store that angle and that index. + * + * After finding the largest index and angle we calculate how much we need to remove + * from the shape radius to move the point inwards by that x. + * + * We average the left and right distances to get the maximum shape radius that can fit in the box + * along with labels. + * + * Once we have that, we can find the centre point for the chart, by taking the x text protrusion + * on each side, removing that from the size, halving it and adding the left x protrusion width. + * + * This will mean we have a shape fitted to the canvas, as large as it can be with the labels + * and position it in the most space efficient manner + * + * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif + */ + + + // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. + // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points + var largestPossibleRadius = helpers.min([(this.height / 2 - this.options.pointLabels.fontSize - 5), this.width / 2]), + pointPosition, + i, + textWidth, + halfTextWidth, + furthestRight = this.width, + furthestRightIndex, + furthestRightAngle, + furthestLeft = 0, + furthestLeftIndex, + furthestLeftAngle, + xProtrusionLeft, + xProtrusionRight, + radiusReductionRight, + radiusReductionLeft, + maxWidthRadius; + this.ctx.font = helpers.fontString(this.options.pointLabels.fontSize, this.options.pointLabels.fontStyle, this.options.pointLabels.fontFamily); + for (i = 0; i < this.valuesCount; i++) { + // 5px to space the text slightly out - similar to what we do in the draw function. + pointPosition = this.getPointPosition(i, largestPossibleRadius); + textWidth = this.ctx.measureText(helpers.template(this.options.labels.template, { + value: this.labels[i] + })).width + 5; + if (i === 0 || i === this.valuesCount / 2) { + // If we're at index zero, or exactly the middle, we're at exactly the top/bottom + // of the radar chart, so text will be aligned centrally, so we'll half it and compare + // w/left and right text sizes + halfTextWidth = textWidth / 2; + if (pointPosition.x + halfTextWidth > furthestRight) { + furthestRight = pointPosition.x + halfTextWidth; + furthestRightIndex = i; + } + if (pointPosition.x - halfTextWidth < furthestLeft) { + furthestLeft = pointPosition.x - halfTextWidth; + furthestLeftIndex = i; + } + } else if (i < this.valuesCount / 2) { + // Less than half the values means we'll left align the text + if (pointPosition.x + textWidth > furthestRight) { + furthestRight = pointPosition.x + textWidth; + furthestRightIndex = i; + } + } else if (i > this.valuesCount / 2) { + // More than half the values means we'll right align the text + if (pointPosition.x - textWidth < furthestLeft) { + furthestLeft = pointPosition.x - textWidth; + furthestLeftIndex = i; + } + } + } + + xProtrusionLeft = furthestLeft; + + xProtrusionRight = Math.ceil(furthestRight - this.width); + + furthestRightAngle = this.getIndexAngle(furthestRightIndex); + + furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); + + radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI / 2); + + radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI / 2); + + // Ensure we actually need to reduce the size of the chart + radiusReductionRight = (helpers.isNumber(radiusReductionRight)) ? radiusReductionRight : 0; + radiusReductionLeft = (helpers.isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; + + this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2; + + //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) + this.setCenterPoint(radiusReductionLeft, radiusReductionRight); + + }, + setCenterPoint: function(leftMovement, rightMovement) { + + var maxRight = this.width - rightMovement - this.drawingArea, + maxLeft = leftMovement + this.drawingArea; + + this.xCenter = (maxLeft + maxRight) / 2; + // Always vertically in the centre as the text height doesn't change + this.yCenter = (this.height / 2); + }, + + getIndexAngle: function(index) { + var angleMultiplier = (Math.PI * 2) / this.valuesCount; + // Start from the top instead of right, so remove a quarter of the circle + + return index * angleMultiplier - (Math.PI / 2); + }, + getPointPosition: function(index, distanceFromCenter) { + var thisAngle = this.getIndexAngle(index); + return { + x: (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, + y: (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter + }; + }, + draw: function() { + if (this.options.display) { + var ctx = this.ctx; + helpers.each(this.yLabels, function(label, index) { + // Don't draw a centre value + if (index > 0) { + var yCenterOffset = index * (this.drawingArea / Math.max(this.ticks.length, 1)), + yHeight = this.yCenter - yCenterOffset, + pointPosition; + + // Draw circular lines around the scale + if (this.options.gridLines.show) { + ctx.strokeStyle = this.options.gridLines.color; + ctx.lineWidth = this.options.gridLines.lineWidth; + + if (this.options.lineArc) { + ctx.beginPath(); + ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI * 2); + ctx.closePath(); + ctx.stroke(); + } else { + ctx.beginPath(); + for (var i = 0; i < this.valuesCount; i++) { + pointPosition = this.getPointPosition(i, this.calculateCenterOffset(this.ticks[index])); + if (i === 0) { + ctx.moveTo(pointPosition.x, pointPosition.y); + } else { + ctx.lineTo(pointPosition.x, pointPosition.y); + } + } + ctx.closePath(); + ctx.stroke(); + } + } + + if (this.options.labels.show) { + ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + + if (this.showLabelBackdrop) { + var labelWidth = ctx.measureText(label).width; + ctx.fillStyle = this.options.labels.backdropColor; + ctx.fillRect( + this.xCenter - labelWidth / 2 - this.options.labels.backdropPaddingX, + yHeight - this.fontSize / 2 - this.options.labels.backdropPaddingY, + labelWidth + this.options.labels.backdropPaddingX * 2, + this.options.labels.fontSize + this.options.lables.backdropPaddingY * 2 + ); + } + + ctx.textAlign = 'center'; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.options.labels.fontColor; + ctx.fillText(label, this.xCenter, yHeight); + } + } + }, this); + + if (!this.options.lineArc) { + ctx.lineWidth = this.options.angleLines.lineWidth; + ctx.strokeStyle = this.options.angleLines.color; + + for (var i = this.valuesCount - 1; i >= 0; i--) { + if (this.options.angleLines.show) { + var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); + ctx.beginPath(); + ctx.moveTo(this.xCenter, this.yCenter); + ctx.lineTo(outerPosition.x, outerPosition.y); + ctx.stroke(); + ctx.closePath(); + } + // Extra 3px out for some label spacing + var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); + ctx.font = helpers.fontString(this.options.pointLabels.fontSize, this.options.pointLabels.fontStyle, this.options.pointLabels.fontFamily); + ctx.fillStyle = this.options.pointLabels.fontColor; + + var labelsCount = this.labels.length, + halfLabelsCount = this.labels.length / 2, + quarterLabelsCount = halfLabelsCount / 2, + upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), + exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); + if (i === 0) { + ctx.textAlign = 'center'; + } else if (i === halfLabelsCount) { + ctx.textAlign = 'center'; + } else if (i < halfLabelsCount) { + ctx.textAlign = 'left'; + } else { + ctx.textAlign = 'right'; + } + + // Set the correct text baseline based on outer positioning + if (exactQuarter) { + ctx.textBaseline = 'middle'; + } else if (upperHalf) { + ctx.textBaseline = 'bottom'; + } else { + ctx.textBaseline = 'top'; + } + + ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); + } + } + } + } + }); + Chart.scales.registerScaleType("radialLinear", LinearRadialScale); }).call(this);