From a2d477ac5ef8e1ffc6c41a8cbcfe158fa591c646 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 24 May 2015 14:33:12 -0400 Subject: [PATCH] Moved radial scale into Chat.Scale.js. Registered as "radialLinear" which will allow radialLogarithmic, etc in the future. Updated the polar area and radar charts to use the new scale config. The scales draw, but the points do not. This is no different than the current v2.0 branch --- src/Chart.Core.js | 265 +----------------------------- src/Chart.PolarArea.js | 161 ++++++++++--------- src/Chart.Radar.js | 220 +++++++++++++------------ src/Chart.Scale.js | 357 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 562 insertions(+), 441 deletions(-) diff --git a/src/Chart.Core.js b/src/Chart.Core.js index 2b6e8a019..a5588028b 100755 --- a/src/Chart.Core.js +++ b/src/Chart.Core.js @@ -1407,7 +1407,7 @@ }, draw: function() { - var ctx = this._chart.ctx; + var ctx = this.ctx; var vm = this._vm; ctx.beginPath(); @@ -1782,269 +1782,6 @@ }, }); - Chart.RadialScale = Chart.Element.extend({ - initialize: function() { - this.size = min([this.height, this.width]); - this.drawingArea = (this.display) ? (this.size / 2) - (this.fontSize / 2 + this.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.lineArc) { - this.setScaleSize(); - } else { - this.drawingArea = (this.display) ? (this.size / 2) - (this.fontSize / 2 + this.backdropPaddingY) : (this.size / 2); - } - this.buildYLabels(); - }, - buildYLabels: function() { - this.yLabels = []; - - var stepDecimalPlaces = getDecimalPlaces(this.stepValue); - - for (var i = 0; i <= this.steps; i++) { - this.yLabels.push(template(this.templateString, { - value: (this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces) - })); - } - }, - 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 = min([(this.height / 2 - this.pointLabelFontSize - 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 = fontString(this.pointLabelFontSize, this.pointLabelFontStyle, this.pointLabelFontFamily); - 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(template(this.templateString, { - 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 = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; - radiusReductionLeft = (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.display) { - var ctx = this.ctx; - each(this.yLabels, function(label, index) { - // Don't draw a centre value - if (index > 0) { - var yCenterOffset = index * (this.drawingArea / this.steps), - yHeight = this.yCenter - yCenterOffset, - pointPosition; - - // Draw circular lines around the scale - if (this.lineWidth > 0) { - ctx.strokeStyle = this.lineColor; - ctx.lineWidth = this.lineWidth; - - if (this.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.min + (index * this.stepValue))); - if (i === 0) { - ctx.moveTo(pointPosition.x, pointPosition.y); - } else { - ctx.lineTo(pointPosition.x, pointPosition.y); - } - } - ctx.closePath(); - ctx.stroke(); - } - } - if (this.showLabels) { - ctx.font = fontString(this.fontSize, this._fontStyle, this._fontFamily); - if (this.showLabelBackdrop) { - var labelWidth = ctx.measureText(label).width; - ctx.fillStyle = this.backdropColor; - ctx.fillRect( - this.xCenter - labelWidth / 2 - this.backdropPaddingX, - yHeight - this.fontSize / 2 - this.backdropPaddingY, - labelWidth + this.backdropPaddingX * 2, - this.fontSize + this.backdropPaddingY * 2 - ); - } - ctx.textAlign = 'center'; - ctx.textBaseline = "middle"; - ctx.fillStyle = this.fontColor; - ctx.fillText(label, this.xCenter, yHeight); - } - } - }, this); - - if (!this.lineArc) { - ctx.lineWidth = this.angleLineWidth; - ctx.strokeStyle = this.angleLineColor; - for (var i = this.valuesCount - 1; i >= 0; i--) { - if (this.angleLineWidth > 0) { - 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 = fontString(this.pointLabelFontSize, this.pointLabelFontStyle, this.pointLabelFontFamily); - ctx.fillStyle = this.pointLabelFontColor; - - 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.animationService = { frameDuration: 17, animations: [], diff --git a/src/Chart.PolarArea.js b/src/Chart.PolarArea.js index eb48a9f0f..49fe2424e 100644 --- a/src/Chart.PolarArea.js +++ b/src/Chart.PolarArea.js @@ -7,24 +7,6 @@ helpers = Chart.helpers; var defaultConfig = { - //Boolean - Show a backdrop to the scale label - scaleShowLabelBackdrop : true, - - //String - The colour of the label backdrop - scaleBackdropColor : "rgba(255,255,255,0.75)", - - // Boolean - Whether the scale should begin at zero - scaleBeginAtZero : true, - - //Number - The backdrop padding above & below the label in pixels - scaleBackdropPaddingY : 2, - - //Number - The backdrop padding to the side of the label in pixels - scaleBackdropPaddingX : 2, - - //Boolean - Show line for each value in the scale - scaleShowLine : true, - //Boolean - Stroke a line around each segment in the chart segmentShowStroke : true, @@ -34,6 +16,48 @@ //Number - The width of the stroke value in pixels segmentStrokeWidth : 2, + scale: { + scaleType: "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%>", + 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, + } + }, + //Number - Amount of animation steps animationSteps : 100, @@ -43,9 +67,6 @@ //Boolean - Whether to animate the rotation of the chart animateRotate : true, - //Boolean - Whether to animate scaling the chart from the centre - animateScale : false, - //String - A legend template legendTemplate : "" }; @@ -70,32 +91,42 @@ x : this.chart.width/2, y : this.chart.height/2 }); - this.scale = new Chart.RadialScale({ - display: this.options.showScale, - fontStyle: this.options.scaleFontStyle, - fontSize: this.options.scaleFontSize, - fontFamily: this.options.scaleFontFamily, - fontColor: this.options.scaleFontColor, - showLabels: this.options.scaleShowLabels, - showLabelBackdrop: this.options.scaleShowLabelBackdrop, - backdropColor: this.options.scaleBackdropColor, - backdropPaddingY : this.options.scaleBackdropPaddingY, - backdropPaddingX: this.options.scaleBackdropPaddingX, - lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, - lineColor: this.options.scaleLineColor, + + var self = this; + var ScaleClass = Chart.scales.getScaleConstructor(this.options.scale.scaleType); + 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, - templateString: this.options.scaleLabel, - valuesCount: this.data.length + valuesCount: this.data.length, + calculateRange: function() { + this.min = null; + this.max = null; + + helpers.each(self.data, function(data) { + if (this.min === null) { + this.min = data.value; + } else if (data.value < this.min) { + this.min = data.value; + } + + if (this.max === null) { + this.max = data.value; + } else if (data.value > this.max) { + this.max = data.value; + } + }, this); + } }); - this.updateScaleRange(this.data); - - this.scale.update(); + this.updateScaleRange(); + this.scale.calculateRange(); + this.scale.generateTicks(); + this.scale.buildYLabels(); helpers.each(this.data,function(segment,index){ this.addData(segment,index,true); @@ -108,9 +139,11 @@ helpers.each(this.segments,function(segment){ segment.restore(["fillColor"]); }); + helpers.each(activeSegments,function(activeSegment){ activeSegment.fillColor = activeSegment.highlightColor; }); + this.showTooltip(activeSegments); }); } @@ -119,12 +152,12 @@ }, getSegmentsAtEvent : function(e){ var segmentsArray = []; - var location = helpers.getRelativePosition(e); helpers.each(this.segments,function(segment){ if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); },this); + return segmentsArray; }, addData : function(segment, atIndex, silent){ @@ -157,37 +190,13 @@ },this); this.scale.valuesCount = this.segments.length; }, - updateScaleRange: function(datapoints){ - var valuesArray = []; - helpers.each(datapoints,function(segment){ - valuesArray.push(segment.value); + 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 }); - var scaleSizes = (this.options.scaleOverride) ? - { - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - } : - helpers.calculateScaleRange( - valuesArray, - helpers.min([this.chart.width, this.chart.height])/2, - this.options.scaleFontSize, - this.options.scaleBeginAtZero, - this.options.scaleIntegersOnly - ); - - helpers.extend( - this.scale, - scaleSizes, - { - size: helpers.min([this.chart.width, this.chart.height]), - xCenter: this.chart.width/2, - yCenter: this.chart.height/2 - } - ); - }, update : function(){ @@ -220,8 +229,11 @@ x : this.chart.width/2, y : this.chart.height/2 }); - this.updateScaleRange(this.segments); - this.scale.update(); + + this.updateScaleRange(); + this.scale.calculateRange(); + this.scale.generateTicks(); + this.scale.buildYLabels(); helpers.extend(this.scale,{ xCenter: this.chart.width/2, @@ -229,8 +241,11 @@ }); helpers.each(this.segments, function(segment){ - segment.update({ - outerRadius : this.scale.calculateCenterOffset(segment.value) + //segment.update({ + // outerRadius : this.scale.calculateCenterOffset(segment.value) + //}); + helpers.extend(segment, { + outerRadius: this.scale.calculateCenterOffset(segment.value) }); }, this); diff --git a/src/Chart.Radar.js b/src/Chart.Radar.js index 636681adc..51795b695 100644 --- a/src/Chart.Radar.js +++ b/src/Chart.Radar.js @@ -10,47 +10,88 @@ Chart.Type.extend({ name: "Radar", defaults:{ - //Boolean - Whether to show lines for each scale point - scaleShowLine : true, - //Boolean - Whether we show the angle lines out of the radar - angleShowLineOut : true, - - //Boolean - Whether to show labels on the scale - scaleShowLabels : false, - - // Boolean - Whether the scale should begin at zero - scaleBeginAtZero : true, - - //String - Colour of the angle line - angleLineColor : "rgba(0,0,0,.1)", - - //Number - Pixel width of the angle line - angleLineWidth : 1, - - //String - Point label font declaration - pointLabelFontFamily : "'Arial'", - - //String - Point label font weight - pointLabelFontStyle : "normal", - - //Number - Point label font size in pixels - pointLabelFontSize : 10, - - //String - Point label font colour - pointLabelFontColor : "#666", + scale: { + scaleType: "radialLinear", + display: true, + + //Boolean - Whether to animate scaling the chart from the centre + animate : false, + + lineArc: false, + + // 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 + }, + + // scale numbers + beginAtZero: true, + + // label settings + labels: { + show: true, + template: "<%=value%>", + 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", + }, + }, //Boolean - Whether to show a dot for each point pointDot : true, //Number - Radius of each point dot in pixels - pointDotRadius : 3, + pointRadius: 3, + + //Number - Pixel width of point dot border + pointBorderWidth: 1, + + //Number - Pixel width of point on hover + pointHoverRadius: 5, - //Number - Pixel width of point dot stroke - pointDotStrokeWidth : 1, + //Number - Pixel width of point dot border on hover + pointHoverBorderWidth: 2, + pointBackgroundColor: Chart.defaults.global.defaultColor, + pointBorderColor: Chart.defaults.global.defaultColor, - //Number - amount extra to add to the radius to cater for hit detection outside the drawn point - pointHitDetectionRadius : 20, + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitRadius: 20, //Boolean - Whether to show a stroke for datasets datasetStroke : true, @@ -68,11 +109,8 @@ initialize: function(){ this.PointClass = Chart.Point.extend({ - strokeWidth : this.options.pointDotStrokeWidth, - radius : this.options.pointDotRadius, display: this.options.pointDot, - hitDetectionRadius : this.options.pointHitDetectionRadius, - ctx : this.chart.ctx + _chart: this.chart }); this.datasets = []; @@ -125,7 +163,15 @@ strokeColor : dataset.pointStrokeColor, fillColor : dataset.pointColor, highlightFill : dataset.pointHighlightFill || dataset.pointColor, - highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor, + + // Appearance + radius: dataset.pointRadius || this.options.pointRadius, + backgroundColor: dataset.pointBackgroundColor || this.options.pointBackgroundColor, + borderWidth: dataset.pointBorderWidth || this.options.pointBorderWidth, + + // Tooltip + hoverRadius: dataset.pointHitRadius || this.options.pointHitRadius, })); },this); @@ -165,78 +211,47 @@ }, buildScale : function(data){ - this.scale = new Chart.RadialScale({ - display: this.options.showScale, - fontStyle: this.options.scaleFontStyle, - fontSize: this.options.scaleFontSize, - fontFamily: this.options.scaleFontFamily, - fontColor: this.options.scaleFontColor, - showLabels: this.options.scaleShowLabels, - showLabelBackdrop: this.options.scaleShowLabelBackdrop, - backdropColor: this.options.scaleBackdropColor, - backdropPaddingY : this.options.scaleBackdropPaddingY, - backdropPaddingX: this.options.scaleBackdropPaddingX, - lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, - lineColor: this.options.scaleLineColor, - angleLineColor : this.options.angleLineColor, - angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, - // Point labels at the edge of each line - pointLabelFontColor : this.options.pointLabelFontColor, - pointLabelFontSize : this.options.pointLabelFontSize, - pointLabelFontFamily : this.options.pointLabelFontFamily, - pointLabelFontStyle : this.options.pointLabelFontStyle, + var self = this; + + var ScaleConstructor = Chart.scales.getScaleConstructor(this.options.scale.scaleType); + 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, - templateString: this.options.scaleLabel, labels: data.labels, - valuesCount: data.datasets[0].data.length + valuesCount: 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.updateScaleRange(data.datasets); + this.scale.calculateRange(); + this.scale.generateTicks(); this.scale.buildYLabels(); }, - updateScaleRange: function(datasets){ - var valuesArray = (function(){ - var totalDataArray = []; - helpers.each(datasets,function(dataset){ - if (dataset.data){ - totalDataArray = totalDataArray.concat(dataset.data); - } - else { - helpers.each(dataset.points, function(point){ - totalDataArray.push(point.value); - }); - } - }); - return totalDataArray; - })(); - - - var scaleSizes = (this.options.scaleOverride) ? - { - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - } : - helpers.calculateScaleRange( - valuesArray, - helpers.min([this.chart.width, this.chart.height])/2, - this.options.scaleFontSize, - this.options.scaleBeginAtZero, - this.options.scaleIntegersOnly - ); - - helpers.extend( - this.scale, - scaleSizes - ); - - }, addData : function(valuesArray,label){ //Map the values array for each of the datasets this.scale.valuesCount++; @@ -308,8 +323,9 @@ xCenter: this.chart.width/2, yCenter: this.chart.height/2 }); - this.updateScaleRange(this.datasets); - this.scale.setScaleSize(); + + this.scale.calculateRange(); + this.scale.generateTicks(); this.scale.buildYLabels(); }, draw : function(ease){ @@ -323,7 +339,7 @@ //Transition each point first so that the line and point drawing isn't out of sync helpers.each(dataset.points,function(point,index){ if (point.hasValue()){ - point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); + point.transition(easeDecimal); } },this); diff --git a/src/Chart.Scale.js b/src/Chart.Scale.js index 7d6436c75..b0313e4c0 100644 --- a/src/Chart.Scale.js +++ b/src/Chart.Scale.js @@ -358,9 +358,9 @@ helpers.each(this.ticks, function(tick, index, ticks) { var label; - if (this.options.labelCallback) { + if (this.options.labels.userCallback) { // If the user provided a callback for label generation, use that as first priority - label = this.options.labelCallback(tick, index, ticks); + label = this.options.lables.userCallback(tick, index, ticks); } else if (this.options.labels.template) { // else fall back to the template string label = helpers.template(this.options.labels.template, { @@ -799,4 +799,357 @@ } }); Chart.scales.registerScaleType("dataset", DatasetScale); + + 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); }).call(this); \ No newline at end of file -- 2.47.3