From b5b00811084e618278550e69e3c247a44163d1fc Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 16 May 2015 19:38:31 -0400 Subject: [PATCH 01/28] Some helper functions needed for better scales. --- src/Chart.Core.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/Chart.Core.js b/src/Chart.Core.js index 939fecf2910..f60d7c2e7e0 100755 --- a/src/Chart.Core.js +++ b/src/Chart.Core.js @@ -360,6 +360,9 @@ toRadians = helpers.radians = function(degrees) { return degrees * (Math.PI / 180); }, + toDegrees = helpers.toDegrees = function(radians) { + return radians * (180 / Math.PI); + }, // Gets the angle from vertical upright to the point about a centre. getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint) { var distanceFromXCenter = anglePoint.x - centrePoint.x, @@ -479,6 +482,36 @@ }; }, + // Implementation of the nice number algorithm used in determining where axis labels will go + niceNum = helpers.niceNum = function(range, round) { + var exponent = Math.floor(Math.log10(range)); + var fraction = range / Math.pow(10, exponent); + var niceFraction; + + if (round) { + if (fraction < 1.5) { + niceFraction = 1; + } else if (fraction < 3) { + niceFraction = 2; + } else if (fraction < 7) { + niceFraction = 5; + } else { + niceFraction = 10; + } + } else { + if (fraction <= 1.0) { + niceFraction = 1; + } else if (fraction <= 2) { + niceFraction = 2; + } else if (fraction <= 5) { + niceFraction = 5; + } else { + niceFraction = 10; + } + } + + return niceFraction * Math.pow(10, exponent); + }, /* jshint ignore:start */ // Blows up jshint errors based on the new Function constructor //Templating methods From db1365c8cec4a682690898f8c0565df486aa6165 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 16 May 2015 19:40:52 -0400 Subject: [PATCH 02/28] Initial check in of the scale service and the scale constructor registration. An initial implementation of a linear scale that can be drawn in both horizontal and vertical orientations. --- src/Chart.Scale.js | 655 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 655 insertions(+) create mode 100644 src/Chart.Scale.js diff --git a/src/Chart.Scale.js b/src/Chart.Scale.js new file mode 100644 index 00000000000..3a2495cbef4 --- /dev/null +++ b/src/Chart.Scale.js @@ -0,0 +1,655 @@ +(function() { + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + // The scale service is used to resize charts along with all of their axes. We make this as + // a service where scales are registered with their respective charts so that changing the + // scales does not require + Chart.scaleService = { + registeredCharts: [], + getWrapperForChart: function(chartInstance) { + var wrapper = helpers.findNextWhere(this.registeredCharts, function(charScaleWrapper) { + return charScaleWrapper.chartInstance == chartInstance; + }); + + return wrapper; + }, + registerChartScale: function(chartInstance, scaleInstance) { + var chartScaleWrapper = this.getWrapperForChart(chartInstance); + + if (!chartScaleWrapper) { + chartScaleWrapper = { + scales: [], + chartInstance: chartInstance, + }; + + this.registeredCharts.push(chartScaleWrapper); + } + + chartScaleWrapper.scales.push(scaleInstance); + }, + removeChartScale: function(chartInstance, scaleInstance) { + var chartScaleWrapper = this.getWrapperForChart(chartInstance); + + if (chartScaleWrapper) { + var scaleIndex = helpers.indexOf(scaleWrapper.scales, scaleInstance); + + if (scaleIndex) { + scaleWrapper.scales.splice(scaleIndex, 1); + } + } + }, + // Remove a chart instance from the scale service. Useful when a chart is destroyed + removeChartInstance: function(chartInstance) { + var index = helpers.findNextWhere(this.registeredCharts, function(scaleWrapper) { + return scaleWrapper.chartInstance == chartInstance; + }); + + if (index) { + this.registeredCharts.splice(index, 1); + } + }, + // The interesting function + fitScalesForChart: function(chartInstance, width, height) { + var chartScaleWrapper = this.getWrapperForChart(chartInstance); + + if (chartScaleWrapper) { + var leftScales = helpers.where(chartScaleWrapper.scales, function(scaleInstance) { + return scaleInstance.options.position == "left"; + }); + var rightScales = helpers.where(chartScaleWrapper.scales, function(scaleInstance) { + return scaleInstance.options.position == "right"; + }); + var topScales = helpers.where(chartScaleWrapper.scales, function(scaleInstance) { + return scaleInstance.options.position == "top"; + }); + var bottomScales = helpers.where(chartScaleWrapper.scales, function(scaleInstance) { + return scaleInstance.options.position == "bottom"; + }); + + // Essentially we now have any number of scales on each of the 4 sides. + // Our canvas looks like the following. + // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and + // B1 is the bottom axis + // |------------------------------------------------------| + // | | T1 | | + // |----|-----|-------------------------------------|-----| + // | | | | | + // | L1 | L2 | Chart area | R1 | + // | | | | | + // | | | | | + // |----|-----|-------------------------------------|-----| + // | | B1 | | + // | | | | + // |------------------------------------------------------| + + // What we do to find the best sizing, we do the following + // 1. Determine the minimum size of the chart area. + // 2. Split the remaining width equally between each vertical axis + // 3. Split the remaining height equally between each horizontal axis + // 4. Give each scale the maximum size it can be. The scale will return it's minimum size + // 5. Adjust the sizes of each axis based on it's minimum reported size. + // 6. Refit each axis + // 7. Position each axis in the final location + // 8. Tell the chart the final location of the chart area + + // Step 1 + var chartWidth = width / 2; // min 50% + var chartHeight = height / 2; // min 50% + var aspectRatio = chartHeight / chartWidth; + var screenAspectRatio; + + if (chartInstance.options.maintainAspectRatio) { + screenAspectRatio = height / width; + + if (aspectRatio != screenAspectRatio) { + chartHeight = chartWidth * screenAspectRatio; + aspectRatio = screenAspectRatio; + } + } + + // Step 2 + var verticalScaleWidth = (width - chartWidth) / (leftScales.length + rightScales.length); + + // Step 3 + var horizontalScaleHeight = (height - chartHeight) / (topScales.length + bottomScales.length); + + // Step 4; + var scalesToMinSize = {}; + + var verticalScaleMinSizeFunction = function(scaleInstance) { + var minSize = scaleInstance.fit(verticalScaleWidth, chartHeight); + scalesToMinSize[scaleInstance] = minSize; + }; + + var horizontalScaleMinSizeFunction = function(scaleInstance) { + var minSize = scaleInstance.fit(chartWidth, horizontalScaleHeight); + scalesToMinSize[scaleInstance] = minSize; + }; + + // vertical scales + helpers.each(leftScales, verticalScaleMinSizeFunction); + helpers.each(rightScales, verticalScaleMinSizeFunction); + + // horizontal scales + helpers.each(topScales, horizontalScaleMinSizeFunction); + helpers.each(bottomScales, horizontalScaleMinSizeFunction); + + // Step 5 + var maxChartHeight = height; + var maxChartWidth = width; + + var chartWidthReduceFunction = function(scaleInstance) { + maxChartWidth -= scalesToMinSize[scaleInstance].width; + }; + + var chartHeightReduceFunction = function(scaleInstance) { + maxChartHeight -= scalesToMinSize[scaleInstance].height; + }; + + helpers.each(leftScales, chartWidthReduceFunction); + helpers.each(rightScales, chartWidthReduceFunction); + helpers.each(topScales, chartHeightReduceFunction); + helpers.each(bottomScales, chartHeightReduceFunction); + + // At this point, maxChartHeight and maxChartWidth are the size the chart area could + // be if the axes are drawn at their minimum sizes. + if (chartInstance.options.maintainAspectRatio) { + // Figure out what the real max size will be + var maxAspectRatio = maxChartHeight / maxChartWidth; + + if (maxAspectRatio != screenAspectRatio) { + // Need to adjust + if (maxChartHeight < maxChartWidth) { + maxChartWidth = maxChartHeight / screenAspectRatio; + } + else { + maxChartHeight = maxChartWidth * screenAspectRatio; + } + } + } + + // Step 6 + var verticalScaleFitFunction = function(scaleInstance) { + var minSize = scalesToMinSize[scaleInstance]; + scaleInstance.fit(minSize.width, maxChartHeight); + }; + + var horizontalScaleFitFunction = function(scaleInstance) { + var minSize = scalesToMinSize[scaleInstance]; + scaleInstance.fit(maxChartWidth, minSize.width); + }; + + helpers.each(leftScales, verticalScaleFitFunction); + helpers.each(rightScales, verticalScaleFitFunction); + helpers.each(topScales, horizontalScaleFitFunction); + helpers.each(bottomScales, horizontalScaleFitFunction); + + // Step 7 + var totalLeftWidth = 0; + var totalTopHeight = 0; + + // Calculate total width of all left axes + helpers.each(leftScales, function(scaleInstance) { + totalLeftWidth += scaleInstance.width; + }); + + // Calculate total height of all top axes + helpers.each(topScales, function(scaleInstance) { + totalTopHeight += scaleInstance.height; + }); + + // Position the scales + var left = 0; + var top = 0; + var right = 0; + var bottom = 0; + + var verticalScalePlacer = function(scaleInstance) { + scaleInstance.left = left; + scaleInstance.right = left + scaleInstance.width; + scaleInstance.top = totalTopHeight; + scaleInstance.bottom = totalTopHeight + maxChartHeight; + + // Move to next point + left = scaleInstance.right; + }; + + var horizontalScalePlacer = function(scaleInstance) { + scaleInstance.left = totalLeftWidth; + scaleInstance.right = totalLeftWidth + maxChartWidth; + scaleInstance.top = top; + scaleInstance.bottom = top + scaleInstance.height; + + // Move to next point + top = scaleInstance.bottom; + }; + + helpers.each(leftScales, verticalScalePlacer); + helpers.each(topScales, horizontalScalePlacer); + + // Account for chart width and height + left += maxChartWidth; + top += maxChartHeight; + + helpers.each(rightScales, verticalScalePlacer); + helpers.each(bottomScales, horizontalScalePlacer); + + // Step 8 + chartScaleWrapper.chartInstance.chartArea = { + left: totalLeftWidth, + top: totalTopHeight, + right: totalLeftWidth + maxChartWidth, + bottom: totalTopHeight + maxChartHeight, + }; + } + } + }; + + // Scale registration object. Extensions can register new scale types (such as log or DB scales) and then + // use the new chart options to grab the correct scale + Chart.scales = { + constructors: {}, + // Use a registration function so that we can move to an ES6 map when we no longer need to support + // old browsers + registerScaleType: function(scaleType, scaleConstructor) { + this.constructors[scaleType] = scaleConstructor; + }, + getScaleConstructor: function(scaleType) { + return this.constructors.hasOwnProperty(scaleType) ? this.constructors[scaleType] : undefined; + } + }; + + var LinearScale = Chart.Element.extend({ + calculateRange: helpers.noop, // overridden in the chart. Will set min and max as properties of the scale for later use + isHorizontal: function() { + return this.options.position == "top" || this.options.position == "bottom"; + }, + generateTicks: function(width, height) { + // 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; + + if (this.isHorizontal()) { + maxTicks = Math.min(11, Math.ceil(width / 50)); + } else { + maxTicks = Math.min(11, Math.ceil(height / 50)); + } + + // 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) { + this.min = Math.min(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(); + } + }, + buildLabels: function() { + // We assume that this has been run after ticks have been generated. We try to figure out + // a label for each tick. + this.labels = []; + + helpers.each(this.ticks, function(tick, index, ticks) { + var label; + + if (this.options.labelCallback) { + // If the user provided a callback for label generation, use that as first priority + label = this.options.labelCallback(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.labels.push(label ? label : ""); // empty string will not render so we're good + }, this); + }, + getPixelForValue: function(value) { + // This must be called after fit has been run so that + // this.left, this.top, this.right, and this.bottom have been defined + var pixel; + var range = this.max - this.min; + + if (this.isHorizontal()) { + pixel = this.left + (this.width / range * (value - this.min)); + } else { + // Bottom - top since pixels increase downard on a screen + pixel = this.bottom - (this.height / range * (value - this.min)); + } + + return pixel; + }, + // Fit this axis to the given size + // @param {number} maxWidth : the max width the axis can be + // @param {number} maxHeight: the max height the axis can be + // @return {object} minSize : the minimum size needed to draw the axis + fit: function(maxWidth, maxHeight) { + this.calculateRange(); + this.generateTicks(maxWidth, maxHeight); + this.buildLabels(); + + var minSize = { + width: 0, + height: 0, + }; + + if (this.isHorizontal()) { + minSize.width = maxWidth; // fill all the width + + // In a horizontal axis, we need some room for the scale to be drawn + // + // ----------------------------------------------------- + // | | | | | + // + minSize.height = this.options.gridLines.show ? 25 : 0; + } else { + minSize.height = maxHeight; // fill all the height + + // In a vertical axis, we need some room for the scale to be drawn. + // The actual grid lines will be drawn on the chart area, however, we need to show + // ticks where the axis actually is. + // We will allocate 25px for this width + // | + // -| + // | + // | + // -| + // | + // | + // -| + minSize.width = this.options.gridLines.show ? 25 : 0; + } + + if (this.options.labels.show) { + // Don't bother fitting the labels if we are not showing them + var labelFont = helpers.fontString(this.options.labels.fontSize, + this.options.labels.fontStyle, this.options.labels.fontFamily); + + if (this.isHorizontal()) { + // A horizontal axis is more constrained by the height. + var maxLabelHeight = maxHeight - minSize.height; + + // Calculate the label rotation + var labelHeight = this.calculateLabelRotation(minSize.width, maxLabelHeight); + minSize.height = Math.min(maxHeight, minSize.height + labelHeight); + } else { + // A vertical axis is more constrained by the width. Labels are the dominant factor + // here, so get that length first + var maxLabelWidth = maxWidth - minSize.width; + var largestTextWidth = helpers.longestText(this.ctx, labelFont, this.labels); + + if (largestTextWidth < maxLabelWidth) { + // We don't need all the room + minSize.width += largestTextWidth; + } else { + // Expand to max size + minSize.width = maxWidth; + } + } + } + + this.width = minSize.width; + this.height = minSize.height; + return minSize; + }, + // Function calculate the needed rotation of the labels. Should only be used in horizontal mode + // @param {number} width : the available width + // @param {number} height: the available height + // @return {number} : the height needed by the labels + calculateLabelRotation : function(width, height){ + //Get the width of each grid by calculating the difference + //between x offsets between 0 and 1. + + var labelFont = helpers.fontString(this.options.labels.fontSize, + this.options.labels.fontStyle, this.options.labels.fontFamily); + + this.labelRotation = 0; // reset + + // Steps + // 1. determine if we need to overlap + // 2. if overlap, determine max rotation + // 3. Rotate until no overlap + // 4. Save rotation + // 5. Return height needed for rotation + var longestTextWidth = helpers.longestText(this.ctx, labelFont, this.lables); + var maxAvailableWidth = (width / (this.ticks.length - 1)) - 6; + + // 6 adds 3px of padding on each end of the label + if (longestTextWidth > maxAvailableWidth) { + // Ok, we need to rotate. Do steps 2-4 + var idealRotation = Math.floor(helpers.toDegrees(Math.asin(height / longestTextWidth))); + var maxRotation = Math.min(90, idealRotation); + + // Increment the rotation in 1 degree steps (step 3) + for (var rotation = 1; rotation < maxRotation; ++rotation) { + var cosRotation = Math.cos(helpers.toRadians(rotation)); + this.labelRotation = rotation; // step 4 + + if (cosRotation * longestTextWidth <= maxAvailableWidth) { + // Rotated enough + break; + } + } + + // step 5 + return Math.min(height, longestTextWidth * Math.sin(this.labelRotation)); + } else { + // Height only constrained by text font size and padding + var idealHeight = this.options.labels.fontSize + 10; // add 10 for padding + return Math.min(height, idealHeight); + } + }, + + // Actualy draw the scale on the canvas + // @param {rectangle} chartArea : the area of the chart to draw full grid lines on + draw: function(chartArea) { + if (this.options.show) { + + var setContextLineSettings; + var hasZero; + + if (this.isHorizontal()) { + if (this.options.gridLines.show) { + // Draw the horizontal line + setContextLineSettings = true; + hasZero = helpers.findNextWhere(this.ticks, function(tick) { return tick === 0; }) !== undefined; + var yTickStart = this.options.position == "bottom" ? this.top : this.bottom - 10; + var yTickEnd = this.options.position == "bottom" ? this.top + 10 : this.bottom; + + this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; + this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; + + this.ctx.beginPath(); + + if (this.options.position == "top") { + this.ctx.moveTo(this.left, this.bottom - 5); + this.ctx.lineTo(this.right, this.bottom - 5); + } else { + // On bottom, so draw horizontal line on the top + this.ctx.moveTo(this.left, this.top + 5); + this.ctx.lineTo(this.right, this.top + 5); + } + + helpers.each(this.ticks, function(tick, index) { + // Grid lines are vertical + var xValue = this.getPixelForValue(tick); + + if (tick === 0 || (!hasZero && index === 0)) { + // Draw the 0 point specially or the left if there is no 0 + this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; + this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; + setContextLineSettings = true; // reset next time + } else if (setContextLineSettings) { + this.ctx.lineWidth = this.options.gridLines.lineWidth; + this.ctx.strokeStyle = this.options.gridLines.color; + setContextLineSettings = false; + } + + xValue += helpers.aliasPixel(this.ctx.lineWidth); + + // Draw the label area + this.ctx.moveTo(xValue, yTickStart); + this.ctx.lineTo(xValue, yTickEnd); + + // Draw the chart area + if (this.options.gridLines.drawOnChartArea) { + this.ctx.moveTo(xValue, chartArea.top); + this.ctx.lineTo(xValue, chartArea.bottom); + } + }, this); + + this.ctx.stroke(); + } + + if (this.options.labels.show) { + // Draw the labels + + var labelStartY; + + if (this.options.position == "top") { + labelStartY = this.top; + } else { + // bottom side + labelStartY = this.top + 20; + } + + this.ctx.textAlign = "center"; + this.ctx.textBaseline = "top"; + this.ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + + helpers.each(this.labels, function(label, index) { + var xValue = this.getPixelForValue(this.ticks[index]); + this.ctx.fillText(label, xValue, labelStartY); + }, this); + } + } else { + // Vertical + if (this.options.gridLines.show) { + + // Draw the vertical line + setContextLineSettings = true; + hasZero = helpers.findNextWhere(this.ticks, function(tick) { return tick === 0; }) !== undefined; + var xTickStart = this.options.position == "left" ? this.left : this.right - 10; + var xTickEnd = this.options.position == "left" ? this.left + 10 : this.right; + + this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; + this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; + + this.ctx.beginPath(); + + if (this.options.position == "left") { + this.ctx.moveTo(this.right - 5, this.top); + this.ctx.lineTo(this.right - 5, this.bottom); + } else { + // On right, so draw vertical line on left size of axis block + this.ctx.moveTo(this.left + 5, this.top); + this.ctx.lineTo(this.left + 5, this.bottom); + } + + helpers.each(this.ticks, function(tick, index) { + // Grid lines are horizontal + var yValue = this.getPixelForValue(tick); + + if (tick === 0 || (!hasZero && index === 0)) { + // Draw the 0 point specially or the bottom if there is no 0 + this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; + this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; + setContextLineSettings = true; // reset next time + } else if (setContextLineSettings) { + this.ctx.lineWidth = this.options.gridLines.lineWidth; + this.ctx.strokeStyle = this.options.gridLines.color; + setContextLineSettings = false; + } + + // Draw the label area + this.ctx.moveTo(xTickStart, yValue); + this.ctx.lineTo(xTickEnd, yValue); + + // Draw the chart area + if (this.options.gridLines.drawOnChartArea) { + this.ctx.moveTo(chartArea.left, yValue); + this.ctx.lineTo(chartArea.right, yValue); + } + }, this); + + this.ctx.stroke(); + } + + if (this.options.labels.show) { + // Draw the labels + + var labelStartX; + var maxLabelWidth = this.width - 25; + + if (this.options.position == "left") { + labelStartX = this.left; + } else { + // right side + labelStartX = this.left + 20; + } + + this.ctx.textAlign = "left"; + this.ctx.textBaseline = "middle"; + this.ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + + helpers.each(this.labels, function(label, index) { + var yValue = this.getPixelForValue(this.ticks[index]); + this.ctx.fillText(label, labelStartX, yValue, maxLabelWidth); + }, this); + } + } + } + } + }); + Chart.scales.registerScaleType("linear", LinearScale); +}).call(this); \ No newline at end of file From d7ad5b6340d1200570b904cce09a1960c8088e55 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 17 May 2015 13:19:37 -0400 Subject: [PATCH 03/28] Add a scatter chart sample --- samples/scatter.html | 145 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 samples/scatter.html diff --git a/samples/scatter.html b/samples/scatter.html new file mode 100644 index 00000000000..afb04edc854 --- /dev/null +++ b/samples/scatter.html @@ -0,0 +1,145 @@ + + + + + Scatter Chart + + + + + +
+
+ +
+
+ + + + + From 67b3d32218e1a27d59d13f8fd4eb88282b59e1ac Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 17 May 2015 13:20:37 -0400 Subject: [PATCH 04/28] Initial implementation of a scatter chart. Uses a new style of config from the other charts. For now, the config is not changeable. --- src/Chart.Scale.js | 53 ++++- src/Chart.Scatter.js | 490 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 536 insertions(+), 7 deletions(-) create mode 100644 src/Chart.Scatter.js diff --git a/src/Chart.Scale.js b/src/Chart.Scale.js index 3a2495cbef4..dcaaefe2ca4 100644 --- a/src/Chart.Scale.js +++ b/src/Chart.Scale.js @@ -55,6 +55,8 @@ // The interesting function fitScalesForChart: function(chartInstance, width, height) { var chartScaleWrapper = this.getWrapperForChart(chartInstance); + var xPadding = 10; + var yPadding = 10; if (chartScaleWrapper) { var leftScales = helpers.where(chartScaleWrapper.scales, function(scaleInstance) { @@ -70,6 +72,31 @@ return scaleInstance.options.position == "bottom"; }); + // Adjust the padding to take into account displaying labels + if (topScales.length == 0 || bottomScales.length == 0) { + var maxFontHeight = 0; + + var maxFontHeightFunction = function(scaleInstance) { + if (scaleInstance.options.labels.show) { + // Only consider font sizes for axes that actually show labels + maxFontHeight = Math.max(maxFontHeight, scaleInstance.options.labels.fontSize); + } + }; + + helpers.each(leftScales, maxFontHeightFunction); + helpers.each(rightScales, maxFontHeightFunction); + + if (topScales.length == 0) { + // Add padding so that we can handle drawing the top nicely + yPadding += 0.75 * maxFontHeight; // 0.75 since padding added on both sides + } + + if (bottomScales.length == 0) { + // Add padding so that we can handle drawing the bottom nicely + yPadding += 1.5 * maxFontHeight; + } + } + // Essentially we now have any number of scales on each of the 4 sides. // Our canvas looks like the following. // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and @@ -111,6 +138,9 @@ } } + chartWidth -= (2 * xPadding); + chartHeight-= (2 * yPadding); + // Step 2 var verticalScaleWidth = (width - chartWidth) / (leftScales.length + rightScales.length); @@ -139,8 +169,8 @@ helpers.each(bottomScales, horizontalScaleMinSizeFunction); // Step 5 - var maxChartHeight = height; - var maxChartWidth = width; + var maxChartHeight = height - (2 * yPadding); + var maxChartWidth = width - (2 * xPadding); var chartWidthReduceFunction = function(scaleInstance) { maxChartWidth -= scalesToMinSize[scaleInstance].width; @@ -189,8 +219,8 @@ helpers.each(bottomScales, horizontalScaleFitFunction); // Step 7 - var totalLeftWidth = 0; - var totalTopHeight = 0; + var totalLeftWidth = xPadding; + var totalTopHeight = yPadding; // Calculate total width of all left axes helpers.each(leftScales, function(scaleInstance) { @@ -203,8 +233,8 @@ }); // Position the scales - var left = 0; - var top = 0; + var left = xPadding; + var top = yPadding; var right = 0; var bottom = 0; @@ -303,7 +333,8 @@ if (this.isHorizontal()) { maxTicks = Math.min(11, Math.ceil(width / 50)); } else { - maxTicks = Math.min(11, Math.ceil(height / 50)); + // The factor of 2 used to scale the font size has been experimentally determined. + maxTicks = Math.min(11, Math.ceil(height / (2 * this.options.labels.fontSize))); } // To get a "nice" value for the tick spacing, we will use the appropriately named @@ -332,6 +363,11 @@ // 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); }, buildLabels: function() { // We assume that this has been run after ticks have been generated. We try to figure out @@ -498,6 +534,9 @@ var setContextLineSettings; var hasZero; + // Make sure we draw text in the correct color + this.ctx.fillStyle = this.options.labels.fontColor; + if (this.isHorizontal()) { if (this.options.gridLines.show) { // Draw the horizontal line diff --git a/src/Chart.Scatter.js b/src/Chart.Scatter.js new file mode 100644 index 00000000000..d1c6b8b2a5b --- /dev/null +++ b/src/Chart.Scatter.js @@ -0,0 +1,490 @@ +(function() { + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + + ///Boolean - Whether grid lines are shown across the chart + scaleShowGridLines: true, + + //String - Colour of the grid lines + scaleGridLineColor: "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth: 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Number - Tension of the bezier curve between points + tension: 0.4, + + //Number - Radius of each point dot in pixels + pointRadius: 4, + + //Number - Pixel width of point dot border + pointBorderWidth: 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHoverRadius: 20, + + //Number - Pixel width of dataset border + borderWidth: 2, + + //String - A legend template + legendTemplate: "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
", + + //Boolean - Whether to horizontally center the label and point dot inside the grid + offsetGridLines: false + + }; + + + Chart.Type.extend({ + name: "Scatter", + defaults: defaultConfig, + initialize: function(data) { + // Save data as a source for updating of values & methods + this.data = data; + + //Custom Point Defaults + this.PointClass = Chart.Point.extend({ + _chart: this.chart, + offsetGridLines: this.options.offsetGridLines, + borderWidth: this.options.pointBorderWidth, + radius: this.options.pointRadius, + hoverRadius: this.options.pointHoverRadius, + }); + + // Events + helpers.bindEvents(this, this.options.tooltipEvents, this.onHover); + + // Build Scale + this.buildScale(this.data.labels); + Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); + + //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(); + dataset.metaData = []; + helpers.each(dataset.data, function(dataPoint, index) { + dataset.metaData.push(new this.PointClass()); + }, this); + }, this); + + // Set defaults for lines + this.eachDataset(function(dataset, datasetIndex) { + dataset = helpers.merge(this.options, dataset); + helpers.extend(dataset.metaDataset, { + _points: dataset.metaData, + _datasetIndex: datasetIndex, + _chart: this.chart, + }); + // Copy to view model + dataset.metaDataset.save(); + }, this); + + // Set defaults for points + this.eachElement(function(point, index, dataset, datasetIndex) { + helpers.extend(point, { + x: this.xScale.getPixelForValue(index), + y: this.chartArea.bottom, + _datasetIndex: datasetIndex, + _index: index, + _chart: this.chart + }); + + // Default bezier control points + helpers.extend(point, { + controlPointPreviousX: this.previousPoint(dataset, index).x, + controlPointPreviousY: this.nextPoint(dataset, index).y, + controlPointNextX: this.previousPoint(dataset, index).x, + controlPointNextY: this.nextPoint(dataset, index).y, + }); + // Copy to view model + point.save(); + }, 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); + + this.update(); + }, + nextPoint: function(collection, index) { + return collection[index - 1] || collection[index]; + }, + previousPoint: function(collection, index) { + return collection[index + 1] || collection[index]; + }, + onHover: function(e) { + // If exiting chart + if (e.type == 'mouseout') { + return this; + } + + this.lastActive = this.lastActive || []; + + // Find Active Elements + this.active = function() { + switch (this.options.hoverMode) { + 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.onHover) { + this.options.onHover.call(this, this.active); + } + + // Remove styling for last active (even if it may still be active) + if (this.lastActive.length) { + switch (this.options.hoverMode) { + case 'single': + this.lastActive[0].backgroundColor = this.data.datasets[this.lastActive[0]._datasetIndex].pointBackgroundColor; + this.lastActive[0].borderColor = this.data.datasets[this.lastActive[0]._datasetIndex].pointBorderColor; + this.lastActive[0].borderWidth = this.data.datasets[this.lastActive[0]._datasetIndex].pointBorderWidth; + break; + case 'label': + for (var i = 0; i < this.lastActive.length; i++) { + this.lastActive[i].backgroundColor = this.data.datasets[this.lastActive[i]._datasetIndex].pointBackgroundColor; + this.lastActive[i].borderColor = this.data.datasets[this.lastActive[i]._datasetIndex].pointBorderColor; + this.lastActive[i].borderWidth = this.data.datasets[this.lastActive[0]._datasetIndex].pointBorderWidth; + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + // Built in hover styling + if (this.active.length && this.options.hoverMode) { + switch (this.options.hoverMode) { + case 'single': + this.active[0].backgroundColor = this.data.datasets[this.active[0]._datasetIndex].hoverBackgroundColor || helpers.color(this.active[0].backgroundColor).saturate(0.5).darken(0.35).rgbString(); + this.active[0].borderColor = this.data.datasets[this.active[0]._datasetIndex].hoverBorderColor || helpers.color(this.active[0].borderColor).saturate(0.5).darken(0.35).rgbString(); + this.active[0].borderWidth = this.data.datasets[this.active[0]._datasetIndex].borderWidth + 10; + break; + case 'label': + for (var i = 0; i < this.active.length; i++) { + this.active[i].backgroundColor = this.data.datasets[this.active[i]._datasetIndex].hoverBackgroundColor || helpers.color(this.active[i].backgroundColor).saturate(0.5).darken(0.35).rgbString(); + this.active[i].borderColor = this.data.datasets[this.active[i]._datasetIndex].hoverBorderColor || helpers.color(this.active[i].borderColor).saturate(0.5).darken(0.35).rgbString(); + this.active[i].borderWidth = this.data.datasets[this.active[i]._datasetIndex].borderWidth + 2; + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + // Built in Tooltips + if (this.options.showTooltips) { + + // The usual updates + this.tooltip.initialize(); + + // Active + if (this.active.length) { + helpers.extend(this.tooltip, { + opacity: 1, + _active: this.active, + }); + + this.tooltip.update(); + } else { + // Inactive + helpers.extend(this.tooltip, { + 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.hoverAnimationDuration); + } + } + + // Remember Last Active + this.lastActive = this.active; + return this; + + }, + update: function() { + Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); + + // Update the lines + this.eachDataset(function(dataset, datasetIndex) { + helpers.extend(dataset.metaDataset, { + backgroundColor: dataset.backgroundColor || this.options.backgroundColor, + borderWidth: dataset.borderWidth || this.options.borderWidth, + borderColor: dataset.borderColor || this.options.borderColor, + tension: dataset.tension || this.options.tension, + scaleTop: this.chartArea.top, + scaleBottom: this.chartArea.bottom, + _points: dataset.metaData, + _datasetIndex: datasetIndex, + }); + dataset.metaDataset.pivot(); + }); + + // Update the points + this.eachElement(function(point, index, dataset, datasetIndex) { + helpers.extend(point, { + x: this.xScale.getPixelForValue(this.data.datasets[datasetIndex].data[index].x), + y: this.yScale.getPixelForValue(this.data.datasets[datasetIndex].data[index].y), + value: this.data.datasets[datasetIndex].data[index].y, + label: this.data.datasets[datasetIndex].data[index].x, + datasetLabel: this.data.datasets[datasetIndex].label, + // Appearance + hoverBackgroundColor: this.data.datasets[datasetIndex].pointHoverBackgroundColor || this.options.pointHoverBackgroundColor, + hoverBorderColor: this.data.datasets[datasetIndex].pointHoverBorderColor || this.options.pointHoverBorderColor, + hoverRadius: this.data.datasets[datasetIndex].pointHoverRadius || this.options.pointHoverRadius, + radius: this.data.datasets[datasetIndex].pointRadius || this.options.pointRadius, + borderWidth: this.data.datasets[datasetIndex].pointBorderWidth || this.options.pointBorderWidth, + borderColor: this.data.datasets[datasetIndex].pointBorderColor || this.options.pointBorderColor, + backgroundColor: this.data.datasets[datasetIndex].pointBackgroundColor || this.options.pointBackgroundColor, + tension: this.data.datasets[datasetIndex].metaDataset.tension, + _datasetIndex: datasetIndex, + _index: index, + }); + }, this); + + // Update control points for the bezier curve + this.eachElement(function(point, index, dataset, datasetIndex) { + var controlPoints = helpers.splineCurve( + this.previousPoint(dataset, index), + point, + this.nextPoint(dataset, index), + point.tension + ); + + point.controlPointPreviousX = controlPoints.previous.x; + point.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.controlPointNextY = this.chartArea.bottom; + } else if (controlPoints.next.y < this.chartArea.top) { + point.controlPointNextY = this.chartArea.top; + } else { + point.controlPointNextY = controlPoints.next.y; + } + + // Cap inner bezier handles to the upper/lower scale bounds + if (controlPoints.previous.y > this.chartArea.bottom) { + point.controlPointPreviousY = this.chartArea.bottom; + } else if (controlPoints.previous.y < this.chartArea.top) { + point.controlPointPreviousY = this.chartArea.top; + } else { + point.controlPointPreviousY = controlPoints.previous.y; + } + // Now pivot the point for animation + point.pivot(); + }, this); + + this.render(); + }, + buildScale: function(labels) { + var self = this; + + var dataTotal = function() { + var values = []; + self.eachValue(function(value) { + values.push(value); + }); + + return values; + }; + + var XScaleClass = Chart.scales.getScaleConstructor("linear"); + var YScaleClass = Chart.scales.getScaleConstructor("linear"); + + this.xScale = new XScaleClass({ + ctx: this.chart.ctx, + }); + + // Eventually this will be referenced from the user supplied config options. + this.xScale.options = { + scaleType: "dataset", // default options are 'dataset', 'linear'. + show: true, + position: "bottom", + horizontal: true, + + // grid line settings + gridLines: { + show: true, + color: "rgba(0, 0, 0, 0.05)", + lineWidth: 1, + drawOnChartArea: true, + }, + + // scale numbers + beginAtZero: false, + integersOnly: false, + override: null, + + // label settings + labels: { + show: true, + template: "<%=value%>", + fontSize: 12, + fontStyle: "normal", + fontColor: "#666", + fontFamily: "Helvetica Neue", + }, + }; + this.yScale = new YScaleClass({ + ctx: this.chart.ctx, + }); + this.yScale.options = { + scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance + show: true, + position: "left", + horizontal: false, + + // grid line settings + gridLines: { + show: true, + color: "rgba(0, 0, 0, 0.05)", + lineWidth: 1, + drawOnChartArea: true, + }, + + // scale numbers + beginAtZero: false, + integersOnly: false, + override: null, + + // label settings + labels: { + show: true, + template: "<%=value%>", + fontSize: 12, + fontStyle: "normal", + fontColor: "#666", + fontFamily: "Helvetica Neue", + }, + }; + + this.xScale.calculateRange = function() { + this.min = null; + this.max = null; + + helpers.each(self.data.datasets, function(dataset) { + helpers.each(dataset.data, function(value) { + if (this.min === null) { + this.min = value.x; + } else if (value.x < this.min) { + this.min = value.x; + } + + if (this.max === null) { + this.max = value.x; + } else if (value.x > this.max) { + this.max = value.x; + } + }, this); + }, this); + }; + + this.yScale.calculateRange = function() { + this.min = null; + this.max = null; + + helpers.each(self.data.datasets, function(dataset) { + helpers.each(dataset.data, function(value) { + if (this.min === null) { + this.min = value.y; + } else if (value.y < this.min) { + this.min = value.y; + } + + if (this.max === null) { + this.max = value.y; + } else if (value.y > this.max) { + this.max = value.y; + } + }, this); + }, this); + }; + + // Register the axes with the scale service + Chart.scaleService.registerChartScale(this, this.xScale); + Chart.scaleService.registerChartScale(this, this.yScale); + }, + redraw: function() { + + }, + draw: function(ease) { + + var easingDecimal = ease || 1; + this.clear(); + + var chartScaleWrapper = Chart.scaleService.getWrapperForChart(this); + + // Draw all the scales + helpers.each(chartScaleWrapper.scales, function(scale) { + scale.draw(this.chartArea); + }, this); + + this.eachDataset(function(dataset, datasetIndex) { + // 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(); + }); + }, this); + + // Finally draw the tooltip + this.tooltip.transition(easingDecimal).draw(); + } + }); + + +}).call(this); From 56d53e3ad482f50df130aa4979d438d5af347e28 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 17 May 2015 13:29:17 -0400 Subject: [PATCH 05/28] Ensure that you always have at least 2 ticks on the linear axis --- src/Chart.Scale.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Chart.Scale.js b/src/Chart.Scale.js index dcaaefe2ca4..1353c9a7f63 100644 --- a/src/Chart.Scale.js +++ b/src/Chart.Scale.js @@ -337,6 +337,9 @@ maxTicks = Math.min(11, Math.ceil(height / (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. From 454f519b6ddc2d60dba0ae2257e0252c6555374c Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Mon, 18 May 2015 08:27:18 -0400 Subject: [PATCH 06/28] Fix jshint warnings --- src/Chart.Scale.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Chart.Scale.js b/src/Chart.Scale.js index 1353c9a7f63..2dd6ace7b3b 100644 --- a/src/Chart.Scale.js +++ b/src/Chart.Scale.js @@ -73,7 +73,7 @@ }); // Adjust the padding to take into account displaying labels - if (topScales.length == 0 || bottomScales.length == 0) { + if (topScales.length === 0 || bottomScales.length === 0) { var maxFontHeight = 0; var maxFontHeightFunction = function(scaleInstance) { @@ -86,12 +86,12 @@ helpers.each(leftScales, maxFontHeightFunction); helpers.each(rightScales, maxFontHeightFunction); - if (topScales.length == 0) { + if (topScales.length === 0) { // Add padding so that we can handle drawing the top nicely yPadding += 0.75 * maxFontHeight; // 0.75 since padding added on both sides } - if (bottomScales.length == 0) { + if (bottomScales.length === 0) { // Add padding so that we can handle drawing the bottom nicely yPadding += 1.5 * maxFontHeight; } From 492292e94b9ba902d6a13329d2a1d6410be29dfb Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Mon, 18 May 2015 08:43:38 -0400 Subject: [PATCH 07/28] Remove x label rotation code since we dynamically adjust the number of labels to show and as such do not need to rotate --- src/Chart.Scale.js | 52 +--------------------------------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/src/Chart.Scale.js b/src/Chart.Scale.js index 2dd6ace7b3b..1f2ae87793f 100644 --- a/src/Chart.Scale.js +++ b/src/Chart.Scale.js @@ -457,9 +457,7 @@ if (this.isHorizontal()) { // A horizontal axis is more constrained by the height. var maxLabelHeight = maxHeight - minSize.height; - - // Calculate the label rotation - var labelHeight = this.calculateLabelRotation(minSize.width, maxLabelHeight); + var labelHeight = 1.5 * this.options.labels.fontSize; minSize.height = Math.min(maxHeight, minSize.height + labelHeight); } else { // A vertical axis is more constrained by the width. Labels are the dominant factor @@ -481,54 +479,6 @@ this.height = minSize.height; return minSize; }, - // Function calculate the needed rotation of the labels. Should only be used in horizontal mode - // @param {number} width : the available width - // @param {number} height: the available height - // @return {number} : the height needed by the labels - calculateLabelRotation : function(width, height){ - //Get the width of each grid by calculating the difference - //between x offsets between 0 and 1. - - var labelFont = helpers.fontString(this.options.labels.fontSize, - this.options.labels.fontStyle, this.options.labels.fontFamily); - - this.labelRotation = 0; // reset - - // Steps - // 1. determine if we need to overlap - // 2. if overlap, determine max rotation - // 3. Rotate until no overlap - // 4. Save rotation - // 5. Return height needed for rotation - var longestTextWidth = helpers.longestText(this.ctx, labelFont, this.lables); - var maxAvailableWidth = (width / (this.ticks.length - 1)) - 6; - - // 6 adds 3px of padding on each end of the label - if (longestTextWidth > maxAvailableWidth) { - // Ok, we need to rotate. Do steps 2-4 - var idealRotation = Math.floor(helpers.toDegrees(Math.asin(height / longestTextWidth))); - var maxRotation = Math.min(90, idealRotation); - - // Increment the rotation in 1 degree steps (step 3) - for (var rotation = 1; rotation < maxRotation; ++rotation) { - var cosRotation = Math.cos(helpers.toRadians(rotation)); - this.labelRotation = rotation; // step 4 - - if (cosRotation * longestTextWidth <= maxAvailableWidth) { - // Rotated enough - break; - } - } - - // step 5 - return Math.min(height, longestTextWidth * Math.sin(this.labelRotation)); - } else { - // Height only constrained by text font size and padding - var idealHeight = this.options.labels.fontSize + 10; // add 10 for padding - return Math.min(height, idealHeight); - } - }, - // Actualy draw the scale on the canvas // @param {rectangle} chartArea : the area of the chart to draw full grid lines on draw: function(chartArea) { From e30a9d2a94f3013a084fb1acd4ceb451d1a07d34 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Mon, 18 May 2015 08:44:06 -0400 Subject: [PATCH 08/28] The scatter example page now has negative numbers in both directions --- samples/scatter.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/scatter.html b/samples/scatter.html index afb04edc854..56c23fa72ca 100644 --- a/samples/scatter.html +++ b/samples/scatter.html @@ -16,7 +16,7 @@ + + + + +
+
+ +
+
+ + + + + diff --git a/src/Chart.Scale.js b/src/Chart.Scale.js index e07aacb0363..3707597b694 100644 --- a/src/Chart.Scale.js +++ b/src/Chart.Scale.js @@ -538,9 +538,11 @@ xValue += helpers.aliasPixel(this.ctx.lineWidth); // Draw the label area - this.ctx.beginPath(); - this.ctx.moveTo(xValue, yTickStart); - this.ctx.lineTo(xValue, yTickEnd); + if (this.options.gridLines.drawTicks) { + this.ctx.beginPath(); + this.ctx.moveTo(xValue, yTickStart); + this.ctx.lineTo(xValue, yTickEnd); + } // Draw the chart area if (this.options.gridLines.drawOnChartArea) { @@ -602,9 +604,11 @@ yValue += helpers.aliasPixel(this.ctx.lineWidth); // Draw the label area - this.ctx.beginPath(); - this.ctx.moveTo(xTickStart, yValue); - this.ctx.lineTo(xTickEnd, yValue); + if (this.options.gridLines.drawTicks) { + this.ctx.beginPath(); + this.ctx.moveTo(xTickStart, yValue); + this.ctx.lineTo(xTickEnd, yValue); + } // Draw the chart area if (this.options.gridLines.drawOnChartArea) { diff --git a/src/Chart.Scatter.js b/src/Chart.Scatter.js index 10aa6757ce5..8267f2a8bc2 100644 --- a/src/Chart.Scatter.js +++ b/src/Chart.Scatter.js @@ -21,6 +21,7 @@ color: "rgba(0, 0, 0, 0.05)", lineWidth: 1, drawOnChartArea: true, + drawTicks: true, zeroLineWidth: 1, zeroLineColor: "rgba(0,0,0,0.25)", }, @@ -53,6 +54,7 @@ color: "rgba(0, 0, 0, 0.05)", lineWidth: 1, drawOnChartArea: true, + drawTicks: true, // draw ticks extending towards the label zeroLineWidth: 1, zeroLineColor: "rgba(0,0,0,0.25)", }, From 4b8c9bc30d9137bc375c3527c8742036ed524377 Mon Sep 17 00:00:00 2001 From: etimberg Date: Mon, 18 May 2015 20:59:21 -0400 Subject: [PATCH 14/28] Fix a drawing bug when drawTicks is false. --- samples/scatter-multi-axis.html | 1 + src/Chart.Scale.js | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/samples/scatter-multi-axis.html b/samples/scatter-multi-axis.html index 8d4658471d0..30473a1ada9 100644 --- a/samples/scatter-multi-axis.html +++ b/samples/scatter-multi-axis.html @@ -95,6 +95,7 @@ hoverMode: 'single', scales: { xAxes: [{ + position: "bottom", gridLines: { zeroLineColor: "rgba(0,0,0,1)" } diff --git a/src/Chart.Scale.js b/src/Chart.Scale.js index 3707597b694..96897f671d2 100644 --- a/src/Chart.Scale.js +++ b/src/Chart.Scale.js @@ -538,8 +538,9 @@ xValue += helpers.aliasPixel(this.ctx.lineWidth); // Draw the label area - if (this.options.gridLines.drawTicks) { - this.ctx.beginPath(); + this.ctx.beginPath(); + + if (this.options.gridLines.drawTicks) { this.ctx.moveTo(xValue, yTickStart); this.ctx.lineTo(xValue, yTickEnd); } @@ -604,8 +605,9 @@ yValue += helpers.aliasPixel(this.ctx.lineWidth); // Draw the label area + this.ctx.beginPath(); + if (this.options.gridLines.drawTicks) { - this.ctx.beginPath(); this.ctx.moveTo(xTickStart, yValue); this.ctx.lineTo(xTickEnd, yValue); } From 4cc24d475ab69047b0cab2d47f214c334ed1b404 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Tue, 19 May 2015 18:22:05 -0400 Subject: [PATCH 15/28] Clean up the scale service. It's now more of a helper function for fitting the scales. Since each chart needed to know it's scales, there wasn't much point in registering them into the service. When we support overlapping charts, we can do something fancier --- src/Chart.Scale.js | 56 +++++--------------------------------------- src/Chart.Scatter.js | 6 +---- 2 files changed, 7 insertions(+), 55 deletions(-) diff --git a/src/Chart.Scale.js b/src/Chart.Scale.js index 96897f671d2..b7db8c1bae4 100644 --- a/src/Chart.Scale.js +++ b/src/Chart.Scale.js @@ -9,66 +9,22 @@ // a service where scales are registered with their respective charts so that changing the // scales does not require Chart.scaleService = { - registeredCharts: [], - getWrapperForChart: function(chartInstance) { - var wrapper = helpers.findNextWhere(this.registeredCharts, function(charScaleWrapper) { - return charScaleWrapper.chartInstance == chartInstance; - }); - - return wrapper; - }, - registerChartScale: function(chartInstance, scaleInstance) { - var chartScaleWrapper = this.getWrapperForChart(chartInstance); - - if (!chartScaleWrapper) { - chartScaleWrapper = { - scales: [], - chartInstance: chartInstance, - }; - - this.registeredCharts.push(chartScaleWrapper); - } - - chartScaleWrapper.scales.push(scaleInstance); - }, - removeChartScale: function(chartInstance, scaleInstance) { - var chartScaleWrapper = this.getWrapperForChart(chartInstance); - - if (chartScaleWrapper) { - var scaleIndex = helpers.indexOf(scaleWrapper.scales, scaleInstance); - - if (scaleIndex) { - scaleWrapper.scales.splice(scaleIndex, 1); - } - } - }, - // Remove a chart instance from the scale service. Useful when a chart is destroyed - removeChartInstance: function(chartInstance) { - var index = helpers.findNextWhere(this.registeredCharts, function(scaleWrapper) { - return scaleWrapper.chartInstance == chartInstance; - }); - - if (index) { - this.registeredCharts.splice(index, 1); - } - }, // The interesting function fitScalesForChart: function(chartInstance, width, height) { - var chartScaleWrapper = this.getWrapperForChart(chartInstance); var xPadding = 10; var yPadding = 10; - if (chartScaleWrapper) { - var leftScales = helpers.where(chartScaleWrapper.scales, function(scaleInstance) { + if (chartInstance) { + var leftScales = helpers.where(chartInstance.scales, function(scaleInstance) { return scaleInstance.options.position == "left"; }); - var rightScales = helpers.where(chartScaleWrapper.scales, function(scaleInstance) { + var rightScales = helpers.where(chartInstance.scales, function(scaleInstance) { return scaleInstance.options.position == "right"; }); - var topScales = helpers.where(chartScaleWrapper.scales, function(scaleInstance) { + var topScales = helpers.where(chartInstance.scales, function(scaleInstance) { return scaleInstance.options.position == "top"; }); - var bottomScales = helpers.where(chartScaleWrapper.scales, function(scaleInstance) { + var bottomScales = helpers.where(chartInstance.scales, function(scaleInstance) { return scaleInstance.options.position == "bottom"; }); @@ -282,7 +238,7 @@ helpers.each(bottomScales, horizontalScalePlacer); // Step 8 - chartScaleWrapper.chartInstance.chartArea = { + chartInstance.chartArea = { left: totalLeftWidth, top: totalTopHeight, right: totalLeftWidth + maxChartWidth, diff --git a/src/Chart.Scatter.js b/src/Chart.Scatter.js index 8267f2a8bc2..d1c2cf10b5e 100644 --- a/src/Chart.Scatter.js +++ b/src/Chart.Scatter.js @@ -451,7 +451,6 @@ }); this.scales[scale.id] = scale; - Chart.scaleService.registerChartScale(this, scale); }, this); helpers.each(this.options.scales.yAxes, function(yAxisOptions) { @@ -464,7 +463,6 @@ }); this.scales[scale.id] = scale; - Chart.scaleService.registerChartScale(this, scale); }, this); }, redraw: function() { @@ -475,10 +473,8 @@ var easingDecimal = ease || 1; this.clear(); - var chartScaleWrapper = Chart.scaleService.getWrapperForChart(this); - // Draw all the scales - helpers.each(chartScaleWrapper.scales, function(scale) { + helpers.each(this.scales, function(scale) { scale.draw(this.chartArea); }, this); From 008bb1aab365c46cbf479cbf9ae2bd98f2350bef Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Tue, 19 May 2015 20:33:39 -0400 Subject: [PATCH 16/28] Set new tooltip templates for the scatter chart --- samples/scatter.html | 2 +- src/Chart.Scatter.js | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/samples/scatter.html b/samples/scatter.html index ecc259f2faf..84cfb2a0e6c 100644 --- a/samples/scatter.html +++ b/samples/scatter.html @@ -88,7 +88,7 @@ var ctx = document.getElementById("canvas").getContext("2d"); window.myScatter = new Chart(ctx).Scatter(scatterChartData, { responsive: true, - hoverMode: 'single', + hoverMode: 'single', // should always use single for a scatter chart scales: { xAxes: [{ gridLines: { diff --git a/src/Chart.Scatter.js b/src/Chart.Scatter.js index d1c2cf10b5e..1de1b06b940 100644 --- a/src/Chart.Scatter.js +++ b/src/Chart.Scatter.js @@ -6,7 +6,7 @@ helpers = Chart.helpers; var defaultConfig = { - + hoverMode: 'single', scales: { xAxes: [{ scaleType: "linear", // scatter should not use a dataset axis @@ -94,6 +94,9 @@ //String - A legend template legendTemplate: "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
", + tooltipTemplate: "(<%= dataX %>, <%= dataY %>)", + multiTooltipTemplate: "<%if (datasetLabel){%><%=datasetLabel%>: <%}%>(<%= dataX %>, <%= dataY %>)", + }; @@ -334,8 +337,10 @@ helpers.extend(point, { x: xScale.getPixelForValue(this.data.datasets[datasetIndex].data[index].x), y: yScale.getPixelForValue(this.data.datasets[datasetIndex].data[index].y), - value: this.data.datasets[datasetIndex].data[index].y, - label: this.data.datasets[datasetIndex].data[index].x, + dataX: this.data.datasets[datasetIndex].data[index].x, + dataY: this.data.datasets[datasetIndex].data[index].y, + label: '', // so that the multitooltip looks ok + value: this.data.datasets[datasetIndex].data[index].y, // for legacy reasons datasetLabel: this.data.datasets[datasetIndex].label, // Appearance hoverBackgroundColor: this.data.datasets[datasetIndex].pointHoverBackgroundColor || this.options.pointHoverBackgroundColor, From ed4b9945f8e428c46c303f5ba6b2228e7485f715 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Wed, 20 May 2015 08:33:26 -0400 Subject: [PATCH 17/28] Initial dataset scale class --- src/Chart.Scale.js | 98 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/Chart.Scale.js b/src/Chart.Scale.js index b7db8c1bae4..71f4a582641 100644 --- a/src/Chart.Scale.js +++ b/src/Chart.Scale.js @@ -605,4 +605,102 @@ } }); Chart.scales.registerScaleType("linear", LinearScale); + + var DatasetScale = Chart.Element.extend({ + // overridden in the chart. Will set min and max as properties of the scale for later use. Min will always be 0 when using a dataset and max will be the number of items in the dataset + calculateRange: helpers.noop, + isHorizontal: function() { + return this.options.position == "top" || this.options.position == "bottom"; + }, + buildLabels: function() { + // We assume that this has been run after ticks have been generated. We try to figure out + // a label for each tick. + + }, + getPixelForValue: function(value, datasetIndex) { + // This must be called after fit has been run so that + // this.left, this.top, this.right, and this.bottom have been defined + if (this.isHorizontal()) { + + } else { + + } + + var isRotated = (this.labelRotation > 0); + var innerWidth = this.width - (this.paddingLeft + this.paddingRight); + var valueWidth = innerWidth / Math.max((this.max - ((this.options.offsetGridLines) ? 0 : 1)), 1); + var valueOffset = (valueWidth * index) + this.paddingLeft; + + if (this.options.offsetGridLines) { + valueOffset += (valueWidth / 2); + } + + return Math.round(valueOffset); + + }, + calculateXLabelRotation: function() { + //Get the width of each grid by calculating the difference + //between x offsets between 0 and 1. + + this.ctx.font = this.font; + + var firstWidth = this.ctx.measureText(this.labels[0]).width; + var lastWidth = this.ctx.measureText(this.labels[this.labels.length - 1]).width; + var firstRotated; + var lastRotated; + + + this.paddingRight = lastWidth / 2 + 3; + this.paddingLeft = firstWidth / 2 + 3; + + this.xLabelRotation = 0; + if (this.display) { + var originalLabelWidth = longestText(this.ctx, this.font, this.labels), + cosRotation, + firstRotatedWidth; + this.xLabelWidth = originalLabelWidth; + //Allow 3 pixels x2 padding either side for label readability + var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; + + //Max label rotate should be 90 - also act as a loop counter + while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)) { + cosRotation = Math.cos(toRadians(this.xLabelRotation)); + + firstRotated = cosRotation * firstWidth; + lastRotated = cosRotation * lastWidth; + + // We're right aligning the text now. + if (firstRotated + this.fontSize / 2 > this.yLabelWidth) { + this.xScalePaddingLeft = firstRotated + this.fontSize / 2; + } + this.xScalePaddingRight = this.fontSize / 2; + + + this.xLabelRotation++; + this.xLabelWidth = cosRotation * originalLabelWidth; + + } + if (this.xLabelRotation > 0) { + this.endPoint -= Math.sin(toRadians(this.xLabelRotation)) * originalLabelWidth + 3; + } + } else { + this.xLabelWidth = 0; + this.xScalePaddingRight = this.padding; + this.xScalePaddingLeft = this.padding; + } + + }, + // Fit this axis to the given size + // @param {number} maxWidth : the max width the axis can be + // @param {number} maxHeight: the max height the axis can be + // @return {object} minSize : the minimum size needed to draw the axis + fit: function(maxWidth, maxHeight) { + + }, + // Actualy draw the scale on the canvas + // @param {rectangle} chartArea : the area of the chart to draw full grid lines on + draw: function(chartArea) { + } + }); + Chart.scales.registerScaleType("dataset", DatasetScale); }).call(this); \ No newline at end of file From b7606c7a8de2ad9fa5b8438d34f1956574c3a7b8 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 23 May 2015 15:15:04 -0400 Subject: [PATCH 18/28] Rename onHover to events to match the line chart --- src/Chart.Scatter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Chart.Scatter.js b/src/Chart.Scatter.js index d7318ad9c0c..5ac3d120e48 100644 --- a/src/Chart.Scatter.js +++ b/src/Chart.Scatter.js @@ -114,7 +114,7 @@ }); // Events - helpers.bindEvents(this, this.options.tooltipEvents, this.onHover); + helpers.bindEvents(this, this.options.tooltipEvents, this.events); // Build Scale this.buildScale(); @@ -188,7 +188,7 @@ previousPoint: function(collection, index) { return collection[index + 1] || collection[index]; }, - onHover: function(e) { + events: function(e) { // If exiting chart if (e.type == 'mouseout') { return this; From c66edc91699e2e8275d36c53bb1d15a19de4cd5b Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 23 May 2015 15:18:08 -0400 Subject: [PATCH 19/28] dataset axis to be used in the line chart and the bar chart --- src/Chart.Scale.js | 184 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 140 insertions(+), 44 deletions(-) diff --git a/src/Chart.Scale.js b/src/Chart.Scale.js index 71f4a582641..2809287702f 100644 --- a/src/Chart.Scale.js +++ b/src/Chart.Scale.js @@ -612,81 +612,81 @@ isHorizontal: function() { return this.options.position == "top" || this.options.position == "bottom"; }, - buildLabels: function() { - // We assume that this has been run after ticks have been generated. We try to figure out - // a label for each tick. - - }, - getPixelForValue: function(value, datasetIndex) { + getPixelForValue: function(value, index, includeOffset) { // This must be called after fit has been run so that // this.left, this.top, this.right, and this.bottom have been defined if (this.isHorizontal()) { + var isRotated = (this.labelRotation > 0); + var innerWidth = this.width - (this.paddingLeft + this.paddingRight); + var valueWidth = innerWidth / Math.max((this.max - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1); + var valueOffset = (valueWidth * index) + this.paddingLeft; - } else { + if (this.options.gridLines.offsetGridLines && includeOffset) { + valueOffset += (valueWidth / 2); + } + return this.left + Math.round(valueOffset); + } else { + return this.top + (index * (this.height / this.max)); } - - var isRotated = (this.labelRotation > 0); - var innerWidth = this.width - (this.paddingLeft + this.paddingRight); - var valueWidth = innerWidth / Math.max((this.max - ((this.options.offsetGridLines) ? 0 : 1)), 1); - var valueOffset = (valueWidth * index) + this.paddingLeft; - - if (this.options.offsetGridLines) { - valueOffset += (valueWidth / 2); - } - - return Math.round(valueOffset); - }, - calculateXLabelRotation: function() { + calculateLabelRotation: function(maxHeight) { //Get the width of each grid by calculating the difference //between x offsets between 0 and 1. - - this.ctx.font = this.font; + var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + this.ctx.font = labelFont; var firstWidth = this.ctx.measureText(this.labels[0]).width; var lastWidth = this.ctx.measureText(this.labels[this.labels.length - 1]).width; var firstRotated; var lastRotated; - this.paddingRight = lastWidth / 2 + 3; this.paddingLeft = firstWidth / 2 + 3; - this.xLabelRotation = 0; - if (this.display) { - var originalLabelWidth = longestText(this.ctx, this.font, this.labels), - cosRotation, - firstRotatedWidth; - this.xLabelWidth = originalLabelWidth; + this.labelRotation = 0; + + if (this.options.show) { + var originalLabelWidth = helpers.longestText(this.ctx, labelFont, this.labels); + var cosRotation; + var sinRotation; + var firstRotatedWidth; + + this.labelWidth = originalLabelWidth; + //Allow 3 pixels x2 padding either side for label readability - var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; + // only the index matters for a dataset scale, but we want a consistent interface between scales + var gridWidth = Math.floor(this.getPixelForValue(0, 1) - this.getPixelForValue(0, 0)) - 6; //Max label rotate should be 90 - also act as a loop counter - while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)) { - cosRotation = Math.cos(toRadians(this.xLabelRotation)); + while ((this.labelWidth > gridWidth && this.labelRotation === 0) || (this.labelWidth > gridWidth && this.labelRotation <= 90 && this.labelRotation > 0)) { + cosRotation = Math.cos(helpers.toRadians(this.labelRotation)); + sinRotation = Math.sin(helpers.toRadians(this.labelRotation)); firstRotated = cosRotation * firstWidth; lastRotated = cosRotation * lastWidth; // We're right aligning the text now. - if (firstRotated + this.fontSize / 2 > this.yLabelWidth) { - this.xScalePaddingLeft = firstRotated + this.fontSize / 2; + if (firstRotated + this.options.labels.fontSize / 2 > this.yLabelWidth) { + this.paddingLeft = firstRotated + this.options.labels.fontSize / 2; } - this.xScalePaddingRight = this.fontSize / 2; + this.paddingRight = this.options.labels.fontSize / 2; - this.xLabelRotation++; - this.xLabelWidth = cosRotation * originalLabelWidth; + if (sinRotation * originalLabelWidth > maxHeight) { + // go back one step + this.labelRotation--; + break; + } + + this.labelRotation++; + this.labelWidth = cosRotation * originalLabelWidth; - } - if (this.xLabelRotation > 0) { - this.endPoint -= Math.sin(toRadians(this.xLabelRotation)) * originalLabelWidth + 3; } } else { - this.xLabelWidth = 0; - this.xScalePaddingRight = this.padding; - this.xScalePaddingLeft = this.padding; + this.labelWidth = 0; + this.paddingRight = this.padding; + this.paddingLeft = this.padding; } }, @@ -695,11 +695,107 @@ // @param {number} maxHeight: the max height the axis can be // @return {object} minSize : the minimum size needed to draw the axis fit: function(maxWidth, maxHeight) { - + this.calculateRange(); + this.calculateLabelRotation(); + + var minSize = { + width: 0, + height: 0, + }; + + var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + var longestLabelWidth = helpers.longestText(this.ctx, labelFont, this.labels); + + if (this.isHorizontal()) { + minSize.width = maxWidth; + this.width = maxWidth; + + var labelHeight = (Math.cos(helpers.toRadians(this.labelRotation)) * longestLabelWidth) + 1.5 * this.options.labels.fontSize; + minSize.height = Math.min(labelHeight, maxHeight); + } else { + minSize.height = maxHeight; + this.height = maxHeight; + + minSize.width = Math.min(longestLabelWidth + 6, maxWidth); + } + + this.width = minSize.width; + this.height = minSize.height; + return minSize; }, // Actualy draw the scale on the canvas // @param {rectangle} chartArea : the area of the chart to draw full grid lines on draw: function(chartArea) { + if (this.options.show) { + + var setContextLineSettings; + + // Make sure we draw text in the correct color + this.ctx.fillStyle = this.options.labels.fontColor; + + if (this.isHorizontal()) { + setContextLineSettings = true; + var yTickStart = this.options.position == "bottom" ? this.top : this.bottom - 10; + var yTickEnd = this.options.position == "bottom" ? this.top + 10 : this.bottom; + var isRotated = this.labelRotation !== 0; + + helpers.each(this.labels, function(label, index) { + var xLineValue = this.getPixelForValue(label, index, false); // xvalues for grid lines + var xLabelValue= this.getPixelForValue(label, index, true); // x values for labels (need to consider offsetLabel option) + + if (this.options.gridLines.show) { + if (index === 0) { + // Draw the first index specially + this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; + this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; + setContextLineSettings = true; // reset next time + } else if (setContextLineSettings) { + this.ctx.lineWidth = this.options.gridLines.lineWidth; + this.ctx.strokeStyle = this.options.gridLines.color; + setContextLineSettings = false; + } + + xLineValue += helpers.aliasPixel(this.ctx.lineWidth); + + // Draw the label area + this.ctx.beginPath(); + + if (this.options.gridLines.drawTicks) { + this.ctx.moveTo(xLineValue, yTickStart); + this.ctx.lineTo(xLineValue, yTickEnd); + } + + // Draw the chart area + if (this.options.gridLines.drawOnChartArea) { + this.ctx.moveTo(xLineValue, chartArea.top); + this.ctx.lineTo(xLineValue, chartArea.bottom); + } + + // Need to stroke in the loop because we are potentially changing line widths & colours + this.ctx.stroke(); + } + + if (this.options.labels.show) { + this.ctx.save(); + this.ctx.translate(xLabelValue, (isRotated) ? this.top + 12 : this.top + 8); + this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1); + this.ctx.font = this.font; + this.ctx.textAlign = (isRotated) ? "right" : "center"; + this.ctx.textBaseline = (isRotated) ? "middle" : "top"; + this.ctx.fillText(label, 0, 0); + this.ctx.restore(); + } + }, this); + } else { + // Vertical + if (this.options.gridLines.show) { + } + + if (this.options.labels.show) { + // Draw the labels + } + } + } } }); Chart.scales.registerScaleType("dataset", DatasetScale); From a559ab85cd16a1e18e14f4aacbbc0fd6f7f75cbd Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 23 May 2015 15:18:33 -0400 Subject: [PATCH 20/28] Ensure consistency in the helpers.toRadians name --- src/Chart.Core.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Chart.Core.js b/src/Chart.Core.js index 0121b11a11a..66f3d1e2997 100755 --- a/src/Chart.Core.js +++ b/src/Chart.Core.js @@ -402,7 +402,7 @@ return 0; } }, - toRadians = helpers.radians = function(degrees) { + toRadians = helpers.toRadians = function(degrees) { return degrees * (Math.PI / 180); }, toDegrees = helpers.toDegrees = function(radians) { From d998c803d36f8a96090dacecbf7c8e330d9b5cbe Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 23 May 2015 15:19:30 -0400 Subject: [PATCH 21/28] Move line chart over to use new scale config. This adds support for multiple y axes. Added an example demonstrating this. --- samples/line-multi-axis.html | 142 +++++++++++++++ samples/line.html | 11 +- src/Chart.Line.js | 335 +++++++++++++++++++++-------------- 3 files changed, 356 insertions(+), 132 deletions(-) create mode 100644 samples/line-multi-axis.html diff --git a/samples/line-multi-axis.html b/samples/line-multi-axis.html new file mode 100644 index 00000000000..85a518cbe94 --- /dev/null +++ b/samples/line-multi-axis.html @@ -0,0 +1,142 @@ + + + + + Line Chart + + + + + +
+
+ +
+
+ + + + + diff --git a/samples/line.html b/samples/line.html index 970c66c12bc..826a568668c 100644 --- a/samples/line.html +++ b/samples/line.html @@ -8,7 +8,7 @@ -
+
@@ -50,7 +50,14 @@ options: { responsive: true, hoverMode: 'label', - stacked: true + stacked: false, + scales: { + xAxes: [{ + gridLines: { + offsetGridLines: false + } + }] + } } }); }; diff --git a/src/Chart.Line.js b/src/Chart.Line.js index f02b63c4950..ed3eae22554 100644 --- a/src/Chart.Line.js +++ b/src/Chart.Line.js @@ -7,29 +7,82 @@ var defaultConfig = { - ///Boolean - Whether grid lines are shown across the chart - scaleShowGridLines: true, - //String - Colour of the grid lines - scaleGridLineColor: "rgba(0,0,0,.05)", - //Number - Width of the grid lines - scaleGridLineWidth: 1, - //Boolean - Whether to show horizontal lines (except X axis) - scaleShowHorizontalLines: true, - //Boolean - Whether to show vertical lines (except Y axis) - scaleShowVerticalLines: true, - //Boolean - Whether to horizontally center the label and point dot inside the grid - offsetGridLines: false, + scales: { + xAxes: [{ + scaleType: "dataset", // scatter should not use a dataset axis + show: true, + position: "bottom", + horizontal: true, + id: "x-axis-1", // need an ID so datasets can reference the scale + + // grid line settings + gridLines: { + show: true, + color: "rgba(0, 0, 0, 0.05)", + lineWidth: 1, + drawOnChartArea: true, + drawTicks: true, + zeroLineWidth: 1, + zeroLineColor: "rgba(0,0,0,0.25)", + offsetGridLines: false, + }, + // scale numbers + beginAtZero: false, + integersOnly: false, + override: null, + + // label settings + labels: { + show: true, + template: "<%=value%>", + fontSize: 12, + fontStyle: "normal", + fontColor: "#666", + fontFamily: "Helvetica Neue", + }, + }], + yAxes: [{ + scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance + show: true, + position: "left", + horizontal: false, + id: "y-axis-1", + + // grid line settings + gridLines: { + show: true, + color: "rgba(0, 0, 0, 0.05)", + lineWidth: 1, + drawOnChartArea: true, + drawTicks: true, // draw ticks extending towards the label + zeroLineWidth: 1, + zeroLineColor: "rgba(0,0,0,0.25)", + }, + // scale numbers + beginAtZero: false, + integersOnly: false, + override: null, + + // label settings + labels: { + show: true, + template: "<%=value%>", + fontSize: 12, + fontStyle: "normal", + fontColor: "#666", + fontFamily: "Helvetica Neue", + } + }], + }, //Boolean - Whether to stack the lines essentially creating a stacked area chart. stacked: false, - //Number - Tension of the bezier curve between points tension: 0.4, - //Number - Radius of each point dot in pixels pointRadius: 3, //Number - Pixel width of point dot border @@ -51,9 +104,6 @@ //String - A legend template legendTemplate: "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
", - - - }; @@ -67,35 +117,6 @@ var _this = this; - // Build Scale - this.ScaleClass = Chart.Scale.extend({ - calculatePointY: function(index, datasetIndex) { - - var value = _this.data.datasets[datasetIndex].data[index]; - - if (_this.options.stacked) { - var offsetPos = 0; - var offsetNeg = 0; - for (var i = 0; i < datasetIndex; i++) { - if (_this.data.datasets[i].data[index] < 0) { - offsetNeg += _this.data.datasets[i].data[index]; - } else { - offsetPos += _this.data.datasets[i].data[index]; - } - } - if (value < 0) { - return this.calculateY(offsetNeg + value); - } else { - return this.calculateY(offsetPos + value); - } - } - - return this.calculateY(value); - } - }); - this.buildScale(this.data.labels); - - //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(); @@ -103,8 +124,19 @@ helpers.each(dataset.data, function(dataPoint, index) { dataset.metaData.push(new Chart.Point()); }, this); + + // The line chart only supports a single x axis because the x axis is always a dataset axis + dataset.xAxisID = this.options.scales.xAxes[0].id; + + if (!dataset.yAxisID) { + dataset.yAxisID = this.options.scales.yAxes[0].id; + } }, this); + // Build and fit the scale. Needs to happen after the axis IDs have been set + this.buildScale(); + Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); + // Set defaults for lines this.eachDataset(function(dataset, datasetIndex) { helpers.extend(dataset.metaDataset, { @@ -120,9 +152,11 @@ // Set defaults for points this.eachElement(function(point, index, dataset, datasetIndex) { + var xScale = this.scales[this.data.datasets[datasetIndex].xAxisID]; + helpers.extend(point, { - x: this.scale.calculateX(index), - y: this.scale.calculateY(0), + x: xScale.getPixelForValue(null, index, true), + y: this.chartArea.bottom, _datasetIndex: datasetIndex, _index: index, _chart: this.chart @@ -155,21 +189,24 @@ return collection[index + 1] || collection[index]; }, update: function() { - - // Update the scale - this.scale.update(); + Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); // Update the lines this.eachDataset(function(dataset, datasetIndex) { + var yScale = this.scales[dataset.yAxisID]; + helpers.extend(dataset.metaDataset, { // Utility _datasetIndex: datasetIndex, + // Data _points: dataset.metaData, + // Geometry - scaleTop: this.scale.startPoint, - scaleBottom: this.scale.endPoint, - scaleZero: this.scale.calculateY(0), + scaleTop: yScale.top, + scaleBottom: yScale.bottom, + scaleZero: yScale.getPixelForValue(0), + // Appearance tension: dataset.tension || this.options.tension, backgroundColor: dataset.backgroundColor || this.options.backgroundColor, @@ -181,24 +218,31 @@ // Update the points this.eachElement(function(point, index, dataset, datasetIndex) { + var xScale = this.scales[this.data.datasets[datasetIndex].xAxisID]; + var yScale = this.scales[this.data.datasets[datasetIndex].yAxisID]; + helpers.extend(point, { // Utility _chart: this.chart, _datasetIndex: datasetIndex, _index: index, + // Data label: this.data.labels[index], value: this.data.datasets[datasetIndex].data[index], datasetLabel: this.data.datasets[datasetIndex].label, + // Geometry offsetGridLines: this.options.offsetGridLines, - x: this.scale.calculateX(index), - y: this.scale.calculatePointY(index, datasetIndex), + x: xScale.getPixelForValue(null, index, true), // value not used in dataset scale, but we want a consistent API between scales + y: yScale.getPointPixelForValue(this.data.datasets[datasetIndex].data[index], index, datasetIndex), tension: this.data.datasets[datasetIndex].metaDataset.tension, + // Appearnce radius: this.data.datasets[datasetIndex].pointRadius || this.options.pointRadius, backgroundColor: this.data.datasets[datasetIndex].pointBackgroundColor || this.options.pointBackgroundColor, borderWidth: this.data.datasets[datasetIndex].pointBorderWidth || this.options.pointBorderWidth, + // Tooltip hoverRadius: this.data.datasets[datasetIndex].pointHitRadius || this.options.pointHitRadius, }); @@ -217,20 +261,21 @@ point.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.scale.endPoint) { - point.controlPointNextY = this.scale.endPoint; - } else if (controlPoints.next.y < this.scale.startPoint) { - point.controlPointNextY = this.scale.startPoint; + if (controlPoints.next.y > this.chartArea.bottom) { + point.controlPointNextY = this.chartArea.bottom; + } else if (controlPoints.next.y < this.chartArea.top) { + point.controlPointNextY = this.chartArea.top; } else { point.controlPointNextY = controlPoints.next.y; } // Cap inner bezier handles to the upper/lower scale bounds - if (controlPoints.previous.y > this.scale.endPoint) { - point.controlPointPreviousY = this.scale.endPoint; - } else if (controlPoints.previous.y < this.scale.startPoint) { - point.controlPointPreviousY = this.scale.startPoint; + if (controlPoints.previous.y > this.chartArea.bottom) { + point.controlPointPreviousY = this.chartArea.bottom; + } else if (controlPoints.previous.y < this.chartArea.top) { + point.controlPointPreviousY = this.chartArea.top; } else { point.controlPointPreviousY = controlPoints.previous.y; } @@ -240,85 +285,112 @@ this.render(); }, - buildScale: function(labels) { + buildScale: function() { var self = this; - var dataTotal = function() { - var values = []; + // Function to determine the range of all the + var calculateYRange = function() { + this.min = null; + this.max = null; + + var positiveValues = []; var negativeValues = []; if (self.options.stacked) { - self.eachValue(function(value, index) { - values[index] = values[index] || 0; - negativeValues[index] = negativeValues[index] || 0; - if (self.options.relativePoints) { - values[index] = 100; - } else { - if (value < 0) { - negativeValues[index] += value; - } else { - values[index] += value; - } + helpers.each(self.data.datasets, function(dataset) { + if (dataset.yAxisID === this.id) { + helpers.each(dataset.data, function(value, index) { + positiveValues[index] = positiveValues[index] || 0; + negativeValues[index] = negativeValues[index] || 0; + + if (self.options.relativePoints) { + positiveValues[index] = 100; + } else { + if (value < 0) { + negativeValues[index] += value; + } else { + positiveValues[index] += value; + } + } + }, this); } - }); - return values.concat(negativeValues); - } - - self.eachValue(function(value, index) { - values.push(value); - }); - - return values; + }, this); + var values = positiveValues.concat(negativeValues); + this.min = helpers.min(values); + this.max = helpers.max(values); + } else { + 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); + } }; - var scaleOptions = { - templateString: this.options.scaleLabel, - height: this.chart.height, - width: this.chart.width, + // Map of scale ID to scale object so we can lookup later + this.scales = {}; + + // Build the x axis. The line chart only supports a single x axis + var ScaleClass = Chart.scales.getScaleConstructor(this.options.scales.xAxes[0].scaleType); + var xScale = new ScaleClass({ ctx: this.chart.ctx, - textColor: this.options.scaleFontColor, - offsetGridLines: this.options.offsetGridLines, - fontSize: this.options.scaleFontSize, - fontStyle: this.options.scaleFontStyle, - fontFamily: this.options.scaleFontFamily, - valuesCount: labels.length, - beginAtZero: this.options.scaleBeginAtZero, - integersOnly: this.options.scaleIntegersOnly, - calculateYRange: function(currentHeight) { - var updatedRanges = helpers.calculateScaleRange( - dataTotal(), - currentHeight, - this.fontSize, - this.beginAtZero, - this.integersOnly - ); - helpers.extend(this, updatedRanges); + options: this.options.scales.xAxes[0], + calculateRange: function() { + this.labels = self.data.labels; + this.min = 0; + this.max = this.labels.length; }, - xLabels: this.data.labels, - font: helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), - lineWidth: this.options.scaleLineWidth, - lineColor: this.options.scaleLineColor, - showHorizontalLines: this.options.scaleShowHorizontalLines, - showVerticalLines: this.options.scaleShowVerticalLines, - gridLineWidth: (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, - gridLineColor: (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", - padding: (this.options.showScale) ? 0 : this.options.pointRadius + this.options.pointBorderWidth, - showLabels: this.options.scaleShowLabels, - display: this.options.showScale - }; + id: this.options.scales.xAxes[0].id, + }); + this.scales[xScale.id] = xScale; + + // Build up all the y scales + helpers.each(this.options.scales.yAxes, function(yAxisOptions) { + var ScaleClass = Chart.scales.getScaleConstructor(yAxisOptions.scaleType); + var scale = new ScaleClass({ + ctx: this.chart.ctx, + options: yAxisOptions, + calculateRange: calculateYRange, + getPointPixelForValue: function(value, index, datasetIndex) { + if (self.options.stacked) { + var offsetPos = 0; + var offsetNeg = 0; + + for (var i = 0; i < datasetIndex; ++i) { + if (self.data.datasets[i].data[index] < 0) { + offsetNeg += self.data.datasets[i].data[index]; + } else { + offsetPos += self.data.datasets[i].data[index]; + } + } - if (this.options.scaleOverride) { - helpers.extend(scaleOptions, { - calculateYRange: helpers.noop, - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + if (value < 0) { + return this.getPixelForValue(offsetNeg + value); + } else { + return this.getPixelForValue(offsetPos + value); + } + } else { + return this.getPixelForValue(value); + } + }, + id: yAxisOptions.id, }); - } - this.scale = new this.ScaleClass(scaleOptions); + this.scales[scale.id] = scale; + }, this); }, redraw: function() { @@ -328,7 +400,10 @@ var easingDecimal = ease || 1; this.clear(); - this.scale.draw(easingDecimal); + // Draw all the scales + helpers.each(this.scales, function(scale) { + scale.draw(this.chartArea); + }, this); // reverse for-loop for proper stacking for (var i = this.data.datasets.length - 1; i >= 0; i--) { From 181e8661752b48f5d1a9e26eb372fdd00d172a02 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 23 May 2015 17:50:27 -0400 Subject: [PATCH 22/28] Fix line chart multi axis sample title --- samples/line-multi-axis.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/line-multi-axis.html b/samples/line-multi-axis.html index 85a518cbe94..d1f9e61959f 100644 --- a/samples/line-multi-axis.html +++ b/samples/line-multi-axis.html @@ -2,7 +2,7 @@ - Line Chart + Line Chart Multiple Axes From a10e245e5ad743686f691d549b2ec3257c7650cb Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 23 May 2015 18:15:57 -0400 Subject: [PATCH 23/28] Use correct tooltip events in each chart --- src/Chart.Doughnut.js | 2 +- src/Chart.PolarArea.js | 2 +- src/Chart.Radar.js | 2 +- src/Chart.Scatter.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Chart.Doughnut.js b/src/Chart.Doughnut.js index c3530650159..b25f48f47b6 100644 --- a/src/Chart.Doughnut.js +++ b/src/Chart.Doughnut.js @@ -53,7 +53,7 @@ //Set up tooltip events on the chart if (this.options.showTooltips) { - helpers.bindEvents(this, this.options.tooltipEvents, this.onHover); + helpers.bindEvents(this, this.options.events, this.onHover); } // Create new slice for each piece of data diff --git a/src/Chart.PolarArea.js b/src/Chart.PolarArea.js index 4bfec4f4697..eb48a9f0f19 100644 --- a/src/Chart.PolarArea.js +++ b/src/Chart.PolarArea.js @@ -103,7 +103,7 @@ //Set up tooltip events on the chart if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + helpers.bindEvents(this, this.options.events, function(evt){ var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; helpers.each(this.segments,function(segment){ segment.restore(["fillColor"]); diff --git a/src/Chart.Radar.js b/src/Chart.Radar.js index ddf12388f92..636681adc4b 100644 --- a/src/Chart.Radar.js +++ b/src/Chart.Radar.js @@ -81,7 +81,7 @@ //Set up tooltip events on the chart if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + helpers.bindEvents(this, this.options.events, function(evt){ var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; this.eachPoints(function(point){ diff --git a/src/Chart.Scatter.js b/src/Chart.Scatter.js index 5ac3d120e48..127b4294e83 100644 --- a/src/Chart.Scatter.js +++ b/src/Chart.Scatter.js @@ -114,7 +114,7 @@ }); // Events - helpers.bindEvents(this, this.options.tooltipEvents, this.events); + helpers.bindEvents(this, this.options.events, this.events); // Build Scale this.buildScale(); From b67afcd727dd1656b531154dd1f2db7cfa5d147c Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 23 May 2015 18:26:34 -0400 Subject: [PATCH 24/28] Bar chart converted to new scale system. Added bar chart multi axis sample. --- samples/bar-multi-axis.html | 134 ++++++++++ samples/bar.html | 2 +- src/Chart.Bar.js | 485 +++++++++++++++++++++--------------- 3 files changed, 419 insertions(+), 202 deletions(-) create mode 100644 samples/bar-multi-axis.html diff --git a/samples/bar-multi-axis.html b/samples/bar-multi-axis.html new file mode 100644 index 00000000000..4a7ccb84892 --- /dev/null +++ b/samples/bar-multi-axis.html @@ -0,0 +1,134 @@ + + + + + Bar Chart Multi Axis + + + + + +
+ +
+ + + + + diff --git a/samples/bar.html b/samples/bar.html index b0f82cf1bf9..d9a5dc4d166 100644 --- a/samples/bar.html +++ b/samples/bar.html @@ -8,7 +8,7 @@ -
+
diff --git a/src/Chart.Bar.js b/src/Chart.Bar.js index e181be4fc8f..a7d3f0e98f7 100644 --- a/src/Chart.Bar.js +++ b/src/Chart.Bar.js @@ -7,23 +7,75 @@ var defaultConfig = { - //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value - scaleBeginAtZero: true, - - //Boolean - Whether grid lines are shown across the chart - scaleShowGridLines: true, - - //String - Colour of the grid lines - scaleGridLineColor: "rgba(0,0,0,.05)", - - //Number - Width of the grid lines - scaleGridLineWidth: 1, + scales: { + xAxes: [{ + scaleType: "dataset", // scatter should not use a dataset axis + show: true, + position: "bottom", + horizontal: true, + id: "x-axis-1", // need an ID so datasets can reference the scale + + // grid line settings + gridLines: { + show: true, + color: "rgba(0, 0, 0, 0.05)", + lineWidth: 1, + drawOnChartArea: true, + drawTicks: true, + zeroLineWidth: 1, + zeroLineColor: "rgba(0,0,0,0.25)", + offsetGridLines: true, + }, - //Boolean - Whether to show horizontal lines (except X axis) - scaleShowHorizontalLines: true, + // scale numbers + beginAtZero: false, + integersOnly: false, + override: null, + + // label settings + labels: { + show: true, + template: "<%=value%>", + fontSize: 12, + fontStyle: "normal", + fontColor: "#666", + fontFamily: "Helvetica Neue", + }, + }], + yAxes: [{ + scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance + show: true, + position: "left", + horizontal: false, + id: "y-axis-1", + + // grid line settings + gridLines: { + show: true, + color: "rgba(0, 0, 0, 0.05)", + lineWidth: 1, + drawOnChartArea: true, + drawTicks: true, // draw ticks extending towards the label + zeroLineWidth: 1, + zeroLineColor: "rgba(0,0,0,0.25)", + }, - //Boolean - Whether to show vertical lines (except Y axis) - scaleShowVerticalLines: true, + // scale numbers + beginAtZero: false, + integersOnly: false, + override: null, + + // label settings + labels: { + show: true, + template: "<%=value%>", + fontSize: 12, + fontStyle: "normal", + fontColor: "#666", + fontFamily: "Helvetica Neue", + } + }], + }, //Number - Pixel width of the bar border barBorderWidth: 2, @@ -47,139 +99,43 @@ name: "Bar", defaults: defaultConfig, initialize: function() { - var options = this.options; - - var _this = this; - - // Custom Scale Methods and Options - this.ScaleClass = Chart.Scale.extend({ - offsetGridLines: true, - calculateBarBase: function(datasetIndex, index) { - - var base = 0; - - if (_this.options.stacked) { - var bar = _this.data.datasets[datasetIndex].metaData[index]; - if (bar.value < 0) { - for (var i = 0; i < datasetIndex; i++) { - base += _this.data.datasets[i].metaData[index].value < base ? _this.data.datasets[i].metaData[index].value : 0; - } - } else { - for (var i = 0; i < datasetIndex; i++) { - base += _this.data.datasets[i].metaData[index].value > base ? _this.data.datasets[i].metaData[index].value : 0; - } - } - return this.calculateY(base); - } - - base = this.endPoint; - - if (this.beginAtZero || ((this.min <= 0 && this.max >= 0) || (this.min >= 0 && this.max <= 0))) { - base = this.calculateY(0); - base += _this.options.scaleGridLineWidth; - } else if (this.min < 0 && this.max < 0) { - // All values are negative. Use the top as the base - base = this.startPoint; - } - - return base; - - }, - calculateBarX: function(datasetCount, datasetIndex, elementIndex) { - var xWidth = this.calculateBaseWidth(), - xAbsolute = this.calculateX(elementIndex) - (xWidth / 2), - barWidth = this.calculateBarWidth(datasetCount); - - if (_this.options.stacked) { - return xAbsolute + barWidth / 2; - } - - return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth / 2; - }, - calculateBarY: function(datasets, datasetIndex, barIndex, value) { - - if (_this.options.stacked) { - - var sumPos = 0, - sumNeg = 0; - - for (var i = 0; i < datasetIndex; i++) { - if (datasets[i].metaData[barIndex].value < 0) { - sumNeg += datasets[i].metaData[barIndex].value || 0; - } else { - sumPos += datasets[i].metaData[barIndex].value || 0; - } - } - - if (value < 0) { - return this.calculateY(sumNeg + value); - } else { - return this.calculateY(sumPos + value); - } - - /*if (options.relativeBars) { - offset = offset / sum * 100; - }*/ - - return this.calculateY(0); - } - - var offset = 0; - - for (i = datasetIndex; i < datasets.length; i++) { - if (i === datasetIndex && value) { - offset += value; - } else { - offset = offset + (datasets[i].metaData[barIndex].value); - } - } - - return this.calculateY(value); - }, - calculateBaseWidth: function() { - return (this.calculateX(1) - this.calculateX(0)) - (2 * options.barValueSpacing); - }, - calculateBaseHeight: function() { - return (this.calculateY(1) - this.calculateY(0)); - }, - calculateBarWidth: function(datasetCount) { - - //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset - var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); - - if (_this.options.stacked) { - return baseWidth; - } - return (baseWidth / datasetCount); - }, - }); - // Events - helpers.bindEvents(this, this.options.tooltipEvents, this.onHover); + helpers.bindEvents(this, this.options.events, this.onHover); //Declare the extension of the default point, to cater for the options passed in to the constructor this.BarClass = Chart.Rectangle.extend({ ctx: this.chart.ctx, }); - // Build Scale - this.buildScale(this.data.labels); - //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 this.BarClass()); }, this); + + // The bar chart only supports a single x axis because the x axis is always a dataset axis + dataset.xAxisID = this.options.scales.xAxes[0].id; + + if (!dataset.yAxisID) { + dataset.yAxisID = this.options.scales.yAxes[0].id; + } }, this); + // Build and fit the scale. Needs to happen after the axis IDs have been set + this.buildScale(); + Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); + // Set defaults for bars this.eachElement(function(bar, index, dataset, datasetIndex) { + var xScale = this.scales[this.data.datasets[datasetIndex].xAxisID]; + var yScale = this.scales[this.data.datasets[datasetIndex].yAxisID]; + helpers.extend(bar, { - base: this.scale.zeroPoint, - width: this.scale.calculateBarWidth(this.data.datasets.length), - x: this.scale.calculateBarX(this.data.datasets.length, datasetIndex, index), - y: this.scale.calculateBarY(this.data.datasets, datasetIndex, index, this.data.datasets[datasetIndex].data[index]), + base: yScale.getPixelForValue(0), + width: xScale.calculateBarWidth(this.data.datasets.length), + x: xScale.calculateBarX(this.data.datasets.length, datasetIndex, index), + y: yScale.calculateBarY(this.data.datasets, datasetIndex, index, this.data.datasets[datasetIndex].data[index]), _datasetIndex: datasetIndex, _index: index, }); @@ -317,22 +273,26 @@ return this; }, update: function() { - - this.scale.update(); + // Update the scale sizes + Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); this.eachElement(function(bar, index, dataset, datasetIndex) { helpers.extend(bar, { value: this.data.datasets[datasetIndex].data[index], }); + bar.pivot(); }, this); this.eachElement(function(bar, index, dataset, datasetIndex) { + var xScale = this.scales[this.data.datasets[datasetIndex].xAxisID]; + var yScale = this.scales[this.data.datasets[datasetIndex].yAxisID]; + helpers.extend(bar, { - base: this.scale.calculateBarBase(datasetIndex, index), - x: this.scale.calculateBarX(this.data.datasets.length, datasetIndex, index), - y: this.scale.calculateBarY(this.data.datasets, datasetIndex, index, this.data.datasets[datasetIndex].data[index]), - width: this.scale.calculateBarWidth(this.data.datasets.length), + base: yScale.calculateBarBase(datasetIndex, index), + x: xScale.calculateBarX(this.data.datasets.length, datasetIndex, index), + y: yScale.calculateBarY(this.data.datasets, datasetIndex, index, this.data.datasets[datasetIndex].data[index]), + width: xScale.calculateBarWidth(this.data.datasets.length), label: this.data.labels[index], datasetLabel: this.data.datasets[datasetIndex].label, borderColor: this.data.datasets[datasetIndex].borderColor, @@ -341,94 +301,214 @@ _datasetIndex: datasetIndex, _index: index, }); + bar.pivot(); }, this); this.render(); }, buildScale: function(labels) { - var self = this; + var self = this; - var dataTotal = function() { - var values = []; + // Function to determine the range of all the + var calculateYRange = function() { + this.min = null; + this.max = null; + + var positiveValues = []; var negativeValues = []; if (self.options.stacked) { - self.eachValue(function(value, index) { - values[index] = values[index] || 0; - negativeValues[index] = negativeValues[index] || 0; - if (self.options.relativeBars) { - values[index] = 100; - } else { - if (value < 0) { - negativeValues[index] += value; - } else { - values[index] += value; - } + helpers.each(self.data.datasets, function(dataset) { + if (dataset.yAxisID === this.id) { + helpers.each(dataset.data, function(value, index) { + positiveValues[index] = positiveValues[index] || 0; + negativeValues[index] = negativeValues[index] || 0; + + if (self.options.relativePoints) { + positiveValues[index] = 100; + } else { + if (value < 0) { + negativeValues[index] += value; + } else { + positiveValues[index] += value; + } + } + }, this); } - }); - return values.concat(negativeValues); + }, this); + + var values = positiveValues.concat(negativeValues); + this.min = helpers.min(values); + this.max = helpers.max(values); + } else { + 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); } + }; - self.eachValue(function(value, index) { - values.push(value); - }); + // Map of scale ID to scale object so we can lookup later + this.scales = {}; - return values; + // Build the x axis. The line chart only supports a single x axis + var ScaleClass = Chart.scales.getScaleConstructor(this.options.scales.xAxes[0].scaleType); + var xScale = new ScaleClass({ + ctx: this.chart.ctx, + options: this.options.scales.xAxes[0], + id: this.options.scales.xAxes[0].id, + calculateRange: function() { + this.labels = self.data.labels; + this.min = 0; + this.max = this.labels.length; + }, + calculateBaseWidth: function() { + return (this.getPixelForValue(null, 1, true) - this.getPixelForValue(null, 0, true)) - (2 * self.options.barValueSpacing); + }, + calculateBarWidth: function(datasetCount) { + //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset + var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * self.options.barDatasetSpacing); - }; + if (self.options.stacked) { + return baseWidth; + } + return (baseWidth / datasetCount); + }, + calculateBarX: function(datasetCount, datasetIndex, elementIndex) { + var xWidth = this.calculateBaseWidth(), + xAbsolute = this.getPixelForValue(null, elementIndex, true) - (xWidth / 2), + barWidth = this.calculateBarWidth(datasetCount); - var scaleOptions = { - templateString: this.options.scaleLabel, - height: this.chart.height, - width: this.chart.width, - ctx: this.chart.ctx, - textColor: this.options.scaleFontColor, - fontSize: this.options.scaleFontSize, - fontStyle: this.options.scaleFontStyle, - fontFamily: this.options.scaleFontFamily, - valuesCount: labels.length, - beginAtZero: this.options.scaleBeginAtZero, - integersOnly: this.options.scaleIntegersOnly, - calculateYRange: function(currentHeight) { - var updatedRanges = helpers.calculateScaleRange( - dataTotal(), - currentHeight, - this.fontSize, - this.beginAtZero, - this.integersOnly - ); - helpers.extend(this, updatedRanges); + if (self.options.stacked) { + return xAbsolute + barWidth / 2; + } + + return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * self.options.barDatasetSpacing) + barWidth / 2; }, - xLabels: labels, - font: helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), - lineWidth: this.options.scaleLineWidth, - lineColor: this.options.scaleLineColor, - showHorizontalLines: this.options.scaleShowHorizontalLines, - showVerticalLines: this.options.scaleShowVerticalLines, - gridLineWidth: (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, - gridLineColor: (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", - padding: (this.options.showScale) ? 0 : this.options.borderWidth, - showLabels: this.options.scaleShowLabels, - display: this.options.showScale - }; + }); + this.scales[xScale.id] = xScale; + + // Build up all the y scales + helpers.each(this.options.scales.yAxes, function(yAxisOptions) { + var ScaleClass = Chart.scales.getScaleConstructor(yAxisOptions.scaleType); + var scale = new ScaleClass({ + ctx: this.chart.ctx, + options: yAxisOptions, + calculateRange: calculateYRange, + calculateBarBase: function(datasetIndex, index) { + var base = 0; + + if (self.options.stacked) { + var bar = self.data.datasets[datasetIndex].metaData[index]; + + if (bar.value < 0) { + for (var i = 0; i < datasetIndex; i++) { + if (self.data.datasets[i].yAxisID === this.id) { + base += self.data.datasets[i].metaData[index].value < base ? self.data.datasets[i].metaData[index].value : 0; + } + } + } else { + for (var i = 0; i < datasetIndex; i++) { + if (self.data.datasets[i].yAxisID === this.id) { + base += self.data.datasets[i].metaData[index].value > base ? self.data.datasets[i].metaData[index].value : 0; + } + } + } + + return this.getPixelForValue(base); + } + + base = this.getPixelForValue(this.min); + + if (this.beginAtZero || ((this.min <= 0 && this.max >= 0) || (this.min >= 0 && this.max <= 0))) { + base = this.getPixelForValue(0); + base += this.options.gridLines.lineWidth; + } else if (this.min < 0 && this.max < 0) { + // All values are negative. Use the top as the base + base = this.getPixelForValue(this.max); + } - if (this.options.scaleOverride) { - helpers.extend(scaleOptions, { - calculateYRange: helpers.noop, - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + return base; + + }, + calculateBarY: function(datasets, datasetIndex, barIndex, value) { + + if (self.options.stacked) { + + var sumPos = 0, + sumNeg = 0; + + for (var i = 0; i < datasetIndex; i++) { + if (datasets[i].metaData[barIndex].value < 0) { + sumNeg += datasets[i].metaData[barIndex].value || 0; + } else { + sumPos += datasets[i].metaData[barIndex].value || 0; + } + } + + if (value < 0) { + return this.getPixelForValue(sumNeg + value); + } else { + return this.getPixelForValue(sumPos + value); + } + + /*if (options.relativeBars) { + offset = offset / sum * 100; + }*/ + + return this.getPixelForValue(0); + } + + var offset = 0; + + for (i = datasetIndex; i < datasets.length; i++) { + if (i === datasetIndex && value) { + offset += value; + } else { + offset = offset + (datasets[i].metaData[barIndex].value); + } + } + + return this.getPixelForValue(value); + }, + + calculateBaseHeight: function() { + return (this.getPixelForValue(1) - this.getPixelForValue(0)); + }, + id: yAxisOptions.id, }); - } - this.scale = new this.ScaleClass(scaleOptions); + this.scales[scale.id] = scale; + }, this); }, // This should be incorportated into the init as something like a default value. "Reflow" seems like a weird word for a fredraw function redraw: function() { - var base = this.scale.zeroPoint; this.eachElement(function(element, index, datasetIndex) { + var yScale = this.scales[this.data.datasets[datasetIndex].yAxisID]; + var base = yScale.getPixelForValue(yScale.min); + + if (yScale.min <= 0 && yScale.max >= 0) { + // have a 0 point + base = yScale.getPixelForValue(0); + } else if (yScale.min < 0 && yScale.max < 0) { + // all megative + base = yScale.getPixelForValue(yScale.max); + } + helpers.extend(element, { y: base, base: base @@ -441,7 +521,10 @@ var easingDecimal = ease || 1; this.clear(); - this.scale.draw(easingDecimal); + // Draw all the scales + helpers.each(this.scales, function(scale) { + scale.draw(this.chartArea); + }, this); //Draw all the bars for each dataset this.eachElement(function(bar, index, datasetIndex) { From 73b579c9626b0f5f5d2ea4d24c36901a8d785cef Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 23 May 2015 21:21:40 -0400 Subject: [PATCH 25/28] Remove old linear scale --- src/Chart.Core.js | 273 ---------------------------------------------- 1 file changed, 273 deletions(-) diff --git a/src/Chart.Core.js b/src/Chart.Core.js index 66f3d1e2997..2b6e8a01930 100755 --- a/src/Chart.Core.js +++ b/src/Chart.Core.js @@ -1782,279 +1782,6 @@ }, }); - Chart.Scale = Chart.Element.extend({ - initialize: function() { - this.fit(); - }, - 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) - })); - } - this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx, this.font, this.yLabels) + 10 : 0; - }, - addXLabel: function(label) { - this.xLabels.push(label); - this.valuesCount++; - this.fit(); - }, - removeXLabel: function() { - this.xLabels.shift(); - this.valuesCount--; - this.fit(); - }, - // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use - fit: function() { - // First we need the width of the yLabels, assuming the xLabels aren't rotated - - // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation - this.startPoint = (this.display) ? this.fontSize : 0; - this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels - - // Apply padding settings to the start and end point. - this.startPoint += this.padding; - this.endPoint -= this.padding; - - // Cache the starting endpoint, excluding the space for x labels - var cachedEndPoint = this.endPoint; - - // Cache the starting height, so can determine if we need to recalculate the scale yAxis - var cachedHeight = this.endPoint - this.startPoint, - cachedYLabelWidth; - - // Build the current yLabels so we have an idea of what size they'll be to start - /* - * This sets what is returned from calculateScaleRange as static properties of this class: - * - this.steps; - this.stepValue; - this.min; - this.max; - * - */ - this.calculateYRange(cachedHeight); - - // With these properties set we can now build the array of yLabels - // and also the width of the largest yLabel - this.buildYLabels(); - - this.calculateXLabelRotation(); - - while ((cachedHeight > this.endPoint - this.startPoint)) { - cachedHeight = this.endPoint - this.startPoint; - cachedYLabelWidth = this.yLabelWidth; - - this.calculateYRange(cachedHeight); - this.buildYLabels(); - - // Only go through the xLabel loop again if the yLabel width has changed - if (cachedYLabelWidth < this.yLabelWidth) { - this.endPoint = cachedEndPoint; - this.calculateXLabelRotation(); - } - } - - }, - calculateXLabelRotation: function() { - //Get the width of each grid by calculating the difference - //between x offsets between 0 and 1. - - this.ctx.font = this.font; - - var firstWidth = this.ctx.measureText(this.xLabels[0]).width, - lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, - firstRotated, - lastRotated; - - - this.xScalePaddingRight = lastWidth / 2 + 3; - this.xScalePaddingLeft = (firstWidth / 2 > this.yLabelWidth) ? firstWidth / 2 : this.yLabelWidth; - - this.xLabelRotation = 0; - if (this.display) { - var originalLabelWidth = longestText(this.ctx, this.font, this.xLabels), - cosRotation, - firstRotatedWidth; - this.xLabelWidth = originalLabelWidth; - //Allow 3 pixels x2 padding either side for label readability - var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; - - //Max label rotate should be 90 - also act as a loop counter - while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)) { - cosRotation = Math.cos(toRadians(this.xLabelRotation)); - - firstRotated = cosRotation * firstWidth; - lastRotated = cosRotation * lastWidth; - - // We're right aligning the text now. - if (firstRotated + this.fontSize / 2 > this.yLabelWidth) { - this.xScalePaddingLeft = firstRotated + this.fontSize / 2; - } - this.xScalePaddingRight = this.fontSize / 2; - - - this.xLabelRotation++; - this.xLabelWidth = cosRotation * originalLabelWidth; - - } - if (this.xLabelRotation > 0) { - this.endPoint -= Math.sin(toRadians(this.xLabelRotation)) * originalLabelWidth + 3; - } - } else { - this.xLabelWidth = 0; - this.xScalePaddingRight = this.padding; - this.xScalePaddingLeft = this.padding; - } - - }, - // Needs to be overidden in each Chart type - // Otherwise we need to pass all the data into the scale class - calculateYRange: noop, - drawingArea: function() { - return this.startPoint - this.endPoint; - }, - calculateY: function(value) { - var scalingFactor = this.drawingArea() / (this.min - this.max); - return this.endPoint - (scalingFactor * (value - this.min)); - }, - calculateX: function(index) { - var isRotated = (this.xLabelRotation > 0), - // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, - innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), - valueWidth = innerWidth / Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1), - valueOffset = (valueWidth * index) + this.xScalePaddingLeft; - - if (this.offsetGridLines) { - valueOffset += (valueWidth / 2); - } - - return Math.round(valueOffset); - }, - update: function(newProps) { - helpers.extend(this, newProps); - this.fit(); - }, - draw: function() { - var ctx = this.ctx, - yLabelGap = (this.endPoint - this.startPoint) / this.steps, - xStart = Math.round(this.xScalePaddingLeft); - if (this.display) { - ctx.fillStyle = this.textColor; - ctx.font = this.font; - each(this.yLabels, function(labelString, index) { - var yLabelCenter = this.endPoint - (yLabelGap * index), - linePositionY = Math.round(yLabelCenter), - drawHorizontalLine = this.showHorizontalLines; - - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - if (this.showLabels) { - ctx.fillText(labelString, xStart - 10, yLabelCenter); - } - - // This is X axis, so draw it - if (index === 0 && !drawHorizontalLine) { - drawHorizontalLine = true; - } - - if (drawHorizontalLine) { - ctx.beginPath(); - } - - if (index > 0) { - // This is a grid line in the centre, so drop that - ctx.lineWidth = this.gridLineWidth; - ctx.strokeStyle = this.gridLineColor; - } else { - // This is the first line on the scale - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - } - - linePositionY += helpers.aliasPixel(ctx.lineWidth); - - if (drawHorizontalLine) { - ctx.moveTo(xStart, linePositionY); - ctx.lineTo(this.width, linePositionY); - ctx.stroke(); - ctx.closePath(); - } - - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - ctx.beginPath(); - ctx.moveTo(xStart - 5, linePositionY); - ctx.lineTo(xStart, linePositionY); - ctx.stroke(); - ctx.closePath(); - - }, this); - - each(this.xLabels, function(label, index) { - var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), - // Check to see if line/bar here and decide where to place the line - linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), - isRotated = (this.xLabelRotation > 0), - drawVerticalLine = this.showVerticalLines; - - // This is Y axis, so draw it - if (index === 0 && !drawVerticalLine) { - drawVerticalLine = true; - } - - if (drawVerticalLine) { - ctx.beginPath(); - } - - if (index > 0) { - // This is a grid line in the centre, so drop that - ctx.lineWidth = this.gridLineWidth; - ctx.strokeStyle = this.gridLineColor; - } else { - // This is the first line on the scale - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - } - - if (drawVerticalLine) { - ctx.moveTo(linePos, this.endPoint); - ctx.lineTo(linePos, this.startPoint - 3); - ctx.stroke(); - ctx.closePath(); - } - - - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - - - // Small lines at the bottom of the base grid line - ctx.beginPath(); - ctx.moveTo(linePos, this.endPoint); - ctx.lineTo(linePos, this.endPoint + 5); - ctx.stroke(); - ctx.closePath(); - - ctx.save(); - ctx.translate(xPos, (isRotated) ? this.endPoint + 12 : this.endPoint + 8); - ctx.rotate(toRadians(this.xLabelRotation) * -1); - ctx.font = this.font; - ctx.textAlign = (isRotated) ? "right" : "center"; - ctx.textBaseline = (isRotated) ? "middle" : "top"; - ctx.fillText(label, 0, 0); - ctx.restore(); - }, this); - - } - } - - }); - Chart.RadialScale = Chart.Element.extend({ initialize: function() { this.size = min([this.height, this.width]); From d4e7765a50ce96ca2dd58ad3657ed310aac9d032 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 23 May 2015 21:44:50 -0400 Subject: [PATCH 26/28] Clean up some unused config options. Renamed 'show' to 'display' for scales since that was clearer and maintains backwards compatibility. --- samples/bar-multi-axis.html | 2 -- samples/line-multi-axis.html | 2 -- samples/scatter-multi-axis.html | 2 -- src/Chart.Bar.js | 4 +--- src/Chart.Line.js | 4 +--- src/Chart.Scale.js | 6 +++--- src/Chart.Scatter.js | 4 +--- 7 files changed, 6 insertions(+), 18 deletions(-) diff --git a/samples/bar-multi-axis.html b/samples/bar-multi-axis.html index 4a7ccb84892..7a54ae0d7cb 100644 --- a/samples/bar-multi-axis.html +++ b/samples/bar-multi-axis.html @@ -54,7 +54,6 @@ scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance show: true, position: "left", - horizontal: false, id: "y-axis-1", // grid line settings @@ -86,7 +85,6 @@ scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance show: true, position: "right", - horizontal: false, id: "y-axis-2", // grid line settings diff --git a/samples/line-multi-axis.html b/samples/line-multi-axis.html index d1f9e61959f..143bc9abc21 100644 --- a/samples/line-multi-axis.html +++ b/samples/line-multi-axis.html @@ -63,7 +63,6 @@ scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance show: true, position: "left", - horizontal: false, id: "y-axis-1", // grid line settings @@ -95,7 +94,6 @@ scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance show: true, position: "right", - horizontal: false, id: "y-axis-2", // grid line settings diff --git a/samples/scatter-multi-axis.html b/samples/scatter-multi-axis.html index a7ca3f96e96..33f00512559 100644 --- a/samples/scatter-multi-axis.html +++ b/samples/scatter-multi-axis.html @@ -106,7 +106,6 @@ scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance show: true, position: "left", - horizontal: false, id: "y-axis-1", // grid line settings @@ -138,7 +137,6 @@ scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance show: true, position: "right", - horizontal: false, id: "y-axis-2", // grid line settings diff --git a/src/Chart.Bar.js b/src/Chart.Bar.js index a7d3f0e98f7..4d737ddb4b2 100644 --- a/src/Chart.Bar.js +++ b/src/Chart.Bar.js @@ -10,9 +10,8 @@ scales: { xAxes: [{ scaleType: "dataset", // scatter should not use a dataset axis - show: true, + display: true, position: "bottom", - horizontal: true, id: "x-axis-1", // need an ID so datasets can reference the scale // grid line settings @@ -46,7 +45,6 @@ scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance show: true, position: "left", - horizontal: false, id: "y-axis-1", // grid line settings diff --git a/src/Chart.Line.js b/src/Chart.Line.js index ed3eae22554..3663843671a 100644 --- a/src/Chart.Line.js +++ b/src/Chart.Line.js @@ -10,9 +10,8 @@ scales: { xAxes: [{ scaleType: "dataset", // scatter should not use a dataset axis - show: true, + display: true, position: "bottom", - horizontal: true, id: "x-axis-1", // need an ID so datasets can reference the scale // grid line settings @@ -46,7 +45,6 @@ scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance show: true, position: "left", - horizontal: false, id: "y-axis-1", // grid line settings diff --git a/src/Chart.Scale.js b/src/Chart.Scale.js index 2809287702f..7d6436c757b 100644 --- a/src/Chart.Scale.js +++ b/src/Chart.Scale.js @@ -460,7 +460,7 @@ // Actualy draw the scale on the canvas // @param {rectangle} chartArea : the area of the chart to draw full grid lines on draw: function(chartArea) { - if (this.options.show) { + if (this.options.display) { var setContextLineSettings; var hasZero; @@ -646,7 +646,7 @@ this.labelRotation = 0; - if (this.options.show) { + if (this.options.display) { var originalLabelWidth = helpers.longestText(this.ctx, labelFont, this.labels); var cosRotation; var sinRotation; @@ -726,7 +726,7 @@ // Actualy draw the scale on the canvas // @param {rectangle} chartArea : the area of the chart to draw full grid lines on draw: function(chartArea) { - if (this.options.show) { + if (this.options.display) { var setContextLineSettings; diff --git a/src/Chart.Scatter.js b/src/Chart.Scatter.js index 127b4294e83..0211c74fc4c 100644 --- a/src/Chart.Scatter.js +++ b/src/Chart.Scatter.js @@ -10,9 +10,8 @@ scales: { xAxes: [{ scaleType: "linear", // scatter should not use a dataset axis - show: true, + display: true, position: "bottom", - horizontal: true, id: "x-axis-1", // need an ID so datasets can reference the scale // grid line settings @@ -45,7 +44,6 @@ scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance show: true, position: "left", - horizontal: false, id: "y-axis-1", // grid line settings From 1352dfeb0fbecde79e9ef0fc17f14026c90b878c Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 23 May 2015 21:49:01 -0400 Subject: [PATCH 27/28] Fix the y scales that were missed in the previous check in --- samples/bar-multi-axis.html | 4 ++-- samples/line-multi-axis.html | 4 ++-- samples/scatter-multi-axis.html | 4 ++-- src/Chart.Bar.js | 2 +- src/Chart.Line.js | 2 +- src/Chart.Scatter.js | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/samples/bar-multi-axis.html b/samples/bar-multi-axis.html index 7a54ae0d7cb..6ae6eca4590 100644 --- a/samples/bar-multi-axis.html +++ b/samples/bar-multi-axis.html @@ -52,7 +52,7 @@ scales: { yAxes: [{ scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance - show: true, + display: true, position: "left", id: "y-axis-1", @@ -83,7 +83,7 @@ } }, { scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance - show: true, + display: true, position: "right", id: "y-axis-2", diff --git a/samples/line-multi-axis.html b/samples/line-multi-axis.html index 143bc9abc21..bb4b58a7791 100644 --- a/samples/line-multi-axis.html +++ b/samples/line-multi-axis.html @@ -61,7 +61,7 @@ }], yAxes: [{ scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance - show: true, + display: true, position: "left", id: "y-axis-1", @@ -92,7 +92,7 @@ } }, { scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance - show: true, + display: true, position: "right", id: "y-axis-2", diff --git a/samples/scatter-multi-axis.html b/samples/scatter-multi-axis.html index 33f00512559..7a85feb4795 100644 --- a/samples/scatter-multi-axis.html +++ b/samples/scatter-multi-axis.html @@ -104,7 +104,7 @@ }], yAxes: [{ scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance - show: true, + display: true, position: "left", id: "y-axis-1", @@ -135,7 +135,7 @@ } }, { scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance - show: true, + display: true, position: "right", id: "y-axis-2", diff --git a/src/Chart.Bar.js b/src/Chart.Bar.js index 4d737ddb4b2..6cc891b4ac3 100644 --- a/src/Chart.Bar.js +++ b/src/Chart.Bar.js @@ -43,7 +43,7 @@ }], yAxes: [{ scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance - show: true, + display: true, position: "left", id: "y-axis-1", diff --git a/src/Chart.Line.js b/src/Chart.Line.js index 3663843671a..ecc87e97f30 100644 --- a/src/Chart.Line.js +++ b/src/Chart.Line.js @@ -43,7 +43,7 @@ }], yAxes: [{ scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance - show: true, + display: true, position: "left", id: "y-axis-1", diff --git a/src/Chart.Scatter.js b/src/Chart.Scatter.js index 0211c74fc4c..240160b7ae7 100644 --- a/src/Chart.Scatter.js +++ b/src/Chart.Scatter.js @@ -42,7 +42,7 @@ }], yAxes: [{ scaleType: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance - show: true, + display: true, position: "left", id: "y-axis-1", From a2d477ac5ef8e1ffc6c41a8cbcfe158fa591c646 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 24 May 2015 14:33:12 -0400 Subject: [PATCH 28/28] 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 2b6e8a01930..a5588028b5c 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 eb48a9f0f19..49fe2424e16 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 : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" }; @@ -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 636681adc4b..51795b695f1 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 7d6436c757b..b0313e4c057 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