diff --git a/samples/bar-multi-axis.html b/samples/bar-multi-axis.html new file mode 100644 index 00000000000..6ae6eca4590 --- /dev/null +++ b/samples/bar-multi-axis.html @@ -0,0 +1,132 @@ + + + + + Bar Chart Multi Axis + + + + + +
+ +
+ + + + + diff --git a/samples/bar.html b/samples/bar.html index 8b0f1e942fc..d9a5dc4d166 100644 --- a/samples/bar.html +++ b/samples/bar.html @@ -8,7 +8,7 @@ -
+
@@ -47,12 +47,15 @@ }; window.onload = function() { var ctx = document.getElementById("canvas").getContext("2d"); - window.myBar = new Chart(ctx).Bar(barChartData, { - responsive: true, - hoverMode: 'label', - scaleBeginAtZero: false, - hoverAnimationDuration: 400, - stacked: true, + window.myBar = new Chart(ctx).Bar({ + data: barChartData, + options: { + responsive: true, + hoverMode: 'label', + scaleBeginAtZero: false, + hoverAnimationDuration: 400, + stacked: true, + } }); }; diff --git a/samples/doughnut.color.html b/samples/doughnut.color.html index 5457fd094fc..f61c25db3e9 100644 --- a/samples/doughnut.color.html +++ b/samples/doughnut.color.html @@ -48,7 +48,12 @@ window.onload = function(){ var ctx = document.getElementById("chart-area").getContext("2d"); - window.myDoughnut = new Chart(ctx).Doughnut(doughnutData, {responsive : true}); + window.myDoughnut = new Chart(ctx).Doughnut({ + data: doughnutData, + options: { + responsive : true + } + }); }; diff --git a/samples/doughnut.html b/samples/doughnut.html index 19c6a6d0cbf..4a5860e3093 100644 --- a/samples/doughnut.html +++ b/samples/doughnut.html @@ -58,8 +58,11 @@ window.onload = function() { var ctx = document.getElementById("chart-area").getContext("2d"); - window.myDoughnut = new Chart(ctx).Doughnut(doughnutData, { - responsive: true + window.myDoughnut = new Chart(ctx).Doughnut({ + data: doughnutData, + options: { + responsive: true + } }); }; diff --git a/samples/line-customTooltips.html b/samples/line-customTooltips.html index 4dc46e1abca..a9ee1b85cd6 100644 --- a/samples/line-customTooltips.html +++ b/samples/line-customTooltips.html @@ -112,10 +112,13 @@ window.onload = function() { var ctx1 = document.getElementById("chart1").getContext("2d"); - window.myLine = new Chart(ctx1).Line(lineChartData, { - showScale: false, - pointDot : true, - responsive: true + window.myLine = new Chart(ctx1).Line({ + data: lineChartData, + options: { + showScale: false, + pointDot : true, + responsive: true + } }); var ctx2 = document.getElementById("chart2").getContext("2d"); diff --git a/samples/line-multi-axis.html b/samples/line-multi-axis.html new file mode 100644 index 00000000000..bb4b58a7791 --- /dev/null +++ b/samples/line-multi-axis.html @@ -0,0 +1,140 @@ + + + + + Line Chart Multiple Axes + + + + + +
+
+ +
+
+ + + + + diff --git a/samples/line.html b/samples/line.html index eac9b32fee1..826a568668c 100644 --- a/samples/line.html +++ b/samples/line.html @@ -8,7 +8,7 @@ -
+
@@ -45,10 +45,20 @@ window.onload = function() { var ctx = document.getElementById("canvas").getContext("2d"); - window.myLine = new Chart(ctx).Line(lineChartData, { - responsive: true, - hoverMode: 'label', - stacked: true + window.myLine = new Chart(ctx).Line({ + data: lineChartData, + options: { + responsive: true, + hoverMode: 'label', + stacked: false, + scales: { + xAxes: [{ + gridLines: { + offsetGridLines: false + } + }] + } + } }); }; diff --git a/samples/pie-customTooltips.html b/samples/pie-customTooltips.html index 732317de464..37075ecbb7c 100644 --- a/samples/pie-customTooltips.html +++ b/samples/pie-customTooltips.html @@ -145,10 +145,14 @@ window.onload = function() { var ctx1 = document.getElementById("chart-area1").getContext("2d"); - window.myPie = new Chart(ctx1).Pie(pieData); + window.myPie = new Chart(ctx1).Pie({ + data: pieData + }); var ctx2 = document.getElementById("chart-area2").getContext("2d"); - window.myPie = new Chart(ctx2).Pie(pieData); + window.myPie = new Chart(ctx2).Pie({ + data: pieData + }); }; diff --git a/samples/pie.html b/samples/pie.html index ca5aecde03a..0bd0b38a6cb 100644 --- a/samples/pie.html +++ b/samples/pie.html @@ -52,7 +52,9 @@ window.onload = function(){ var ctx = document.getElementById("chart-area").getContext("2d"); - window.myPie = new Chart(ctx).Pie(pieData); + window.myPie = new Chart(ctx).Pie({ + data: pieData + }); }; $('#randomizeData').click(function(){ diff --git a/samples/polar-area.html b/samples/polar-area.html index 0828c102034..51682e65b9d 100644 --- a/samples/polar-area.html +++ b/samples/polar-area.html @@ -53,8 +53,11 @@ window.onload = function(){ var ctx = document.getElementById("chart-area").getContext("2d"); - window.myPolarArea = new Chart(ctx).PolarArea(polarData, { - responsive:true + window.myPolarArea = new Chart(ctx).PolarArea({ + data: polarData, + options: { + responsive:true + } }); }; diff --git a/samples/radar.html b/samples/radar.html index d260797847a..0885f61ab8a 100644 --- a/samples/radar.html +++ b/samples/radar.html @@ -44,8 +44,11 @@ }; window.onload = function(){ - window.myRadar = new Chart(document.getElementById("canvas").getContext("2d")).Radar(radarChartData, { - responsive: true + window.myRadar = new Chart(document.getElementById("canvas").getContext("2d")).Radar({ + data: radarChartData, + options: { + responsive: true + } }); } diff --git a/samples/scatter-multi-axis.html b/samples/scatter-multi-axis.html new file mode 100644 index 00000000000..7a85feb4795 --- /dev/null +++ b/samples/scatter-multi-axis.html @@ -0,0 +1,223 @@ + + + + + Scatter Chart Multi Axis + + + + + +
+
+ +
+
+ + + + + diff --git a/samples/scatter.html b/samples/scatter.html new file mode 100644 index 00000000000..b27743762c1 --- /dev/null +++ b/samples/scatter.html @@ -0,0 +1,155 @@ + + + + + Scatter Chart + + + + + +
+
+ +
+
+ + + + + diff --git a/src/Chart.Bar.js b/src/Chart.Bar.js index ee9c7921ed8..6cc891b4ac3 100644 --- a/src/Chart.Bar.js +++ b/src/Chart.Bar.js @@ -7,23 +7,73 @@ 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 + display: true, + position: "bottom", + 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 + display: true, + position: "left", + 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, @@ -46,144 +96,44 @@ Chart.Type.extend({ name: "Bar", defaults: defaultConfig, - initialize: function(data) { - - // Save data as a source for updating of values & methods - this.data = data; - - 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); - }, - }); - + initialize: function() { // 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, }); @@ -321,22 +271,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, @@ -345,94 +299,214 @@ _datasetIndex: datasetIndex, _index: index, }); + bar.pivot(); }, this); this.render(); }, buildScale: function(labels) { - var self = this; + var self = this; + + // Function to determine the range of all the + var calculateYRange = function() { + this.min = null; + this.max = null; - var dataTotal = function() { - var values = []; + 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 = {}; + + // 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); - return values; + 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); - }; + if (self.options.stacked) { + return xAbsolute + barWidth / 2; + } - 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); + 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 @@ -445,7 +519,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) { diff --git a/src/Chart.Core.js b/src/Chart.Core.js index 9d23a580158..a5588028b5c 100755 --- a/src/Chart.Core.js +++ b/src/Chart.Core.js @@ -242,6 +242,37 @@ args.unshift({}); return extend.apply(null, args); }, + // Need a special merge function to chart configs since they are now grouped + configMerge = helpers.configMerge = function(base) { + helpers.each(Array.prototype.slice.call(arguments, 1), function(extension) { + helpers.each(extension, function(value, key) { + if (extension.hasOwnProperty(key)) { + if (base.hasOwnProperty(key) && helpers.isArray(base[key]) && helpers.isArray(value)) { + // In this case we have an array of objects replacing another array. Rather than doing a strict replace, + // merge. This allows easy scale option merging + var baseArray = base[key]; + + helpers.each(value, function(valueObj, index) { + if (index < baseArray.length) { + baseArray[index] = helpers.configMerge(baseArray[index], valueObj); + } else { + baseArray.push(valueObj); // nothing to merge + } + }); + } + else if (base.hasOwnProperty(key) && typeof base[key] == "object" && typeof value == "object") { + // If we are overwriting an object with an object, do a merge of the properties. + base[key] = helpers.configMerge(base[key], value); + } else { + // can just overwrite the value in this case + base[key] = value; + } + } + }); + }); + + return base; + }, indexOf = helpers.indexOf = function(arrayToSearch, item) { if (Array.prototype.indexOf) { return arrayToSearch.indexOf(item); @@ -330,6 +361,17 @@ min = helpers.min = function(array) { return Math.min.apply(Math, array); }, + sign = helpers.sign = function(x) { + if (Math.sign) { + return Math.sign(x); + } else { + x = +x; // convert to a number + if (x === 0 || isNaN(x)) { + return x; + } + return x > 0 ? 1 : -1; + } + }, cap = helpers.cap = function(valueToCap, maxValue, minValue) { if (isNumber(maxValue)) { if (valueToCap > maxValue) { @@ -360,9 +402,12 @@ return 0; } }, - toRadians = helpers.radians = function(degrees) { + toRadians = helpers.toRadians = 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, @@ -482,6 +527,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 @@ -1086,13 +1161,13 @@ var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; - Chart.defaults[chartName] = extend(baseDefaults, extensions.defaults); + Chart.defaults[chartName] = helpers.configMerge(baseDefaults, extensions.defaults); Chart.types[chartName] = ChartType; //Register this new chart type in the Chart prototype Chart.prototype[chartName] = function(config) { - helpers.extend(config.options, merge(Chart.defaults.global, Chart.defaults[chartName], config.options || {})); + config.options = helpers.configMerge(Chart.defaults.global, Chart.defaults[chartName], config.options || {}); return new ChartType(config, this); }; } else { @@ -1332,7 +1407,7 @@ }, draw: function() { - var ctx = this._chart.ctx; + var ctx = this.ctx; var vm = this._vm; ctx.beginPath(); @@ -1707,542 +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]); - 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.Doughnut.js b/src/Chart.Doughnut.js index 1fc05581c79..b25f48f47b6 100644 --- a/src/Chart.Doughnut.js +++ b/src/Chart.Doughnut.js @@ -43,11 +43,7 @@ defaults: defaultConfig, //Initialize is fired when the chart is initialized - Data is passed in as a parameter //Config is automatically merged by the core of Chart.js, and is available at this.options - initialize: function(data) { - - // Save data as a source for updating of values & methods - this.data = data; - + initialize: function() { // Slice Type and defaults this.Slice = Chart.Arc.extend({ _chart: this.chart, @@ -57,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.Line.js b/src/Chart.Line.js index f02b63c4950..ecc87e97f30 100644 --- a/src/Chart.Line.js +++ b/src/Chart.Line.js @@ -7,29 +7,80 @@ 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 + display: true, + position: "bottom", + 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 + display: true, + position: "left", + 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 +102,6 @@ //String - A legend template legendTemplate: "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
", - - - }; @@ -67,35 +115,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 +122,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 +150,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 +187,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 +216,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 +259,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 +283,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 +398,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--) { diff --git a/src/Chart.PolarArea.js b/src/Chart.PolarArea.js index c220a6f4b67..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%><%}%>
  • <%}%>
" }; @@ -58,10 +79,7 @@ defaults : defaultConfig, //Initialize is fired when the chart is initialized - Data is passed in as a parameter //Config is automatically merged by the core of Chart.js, and is available at this.options - initialize: function(data){ - // Save data as a source for updating of values & methods - this.data = data; - + initialize: function(){ this.segments = []; //Declare segment class as a chart instance specific class, so it can share props for this instance this.SegmentArc = Chart.Arc.extend({ @@ -73,47 +91,59 @@ 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: 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(data); - - this.scale.update(); + this.updateScaleRange(); + this.scale.calculateRange(); + this.scale.generateTicks(); + this.scale.buildYLabels(); - helpers.each(data,function(segment,index){ + helpers.each(this.data,function(segment,index){ this.addData(segment,index,true); },this); //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"]); }); + helpers.each(activeSegments,function(activeSegment){ activeSegment.fillColor = activeSegment.highlightColor; }); + this.showTooltip(activeSegments); }); } @@ -122,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){ @@ -160,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(){ @@ -223,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, @@ -232,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 b033c8bb2f8..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 dot stroke - pointDotStrokeWidth : 1, + //Number - Pixel width of point on hover + pointHoverRadius: 5, - //Number - amount extra to add to the radius to cater for hit detection outside the drawn point - pointHitDetectionRadius : 20, + //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 + pointHitRadius: 20, //Boolean - Whether to show a stroke for datasets datasetStroke : true, @@ -66,25 +107,19 @@ }, - initialize: function(data){ - // Save data as a source for updating of values & methods - this.data = data; - + 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 = []; - this.buildScale(data); + this.buildScale(this.data); //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){ @@ -100,7 +135,7 @@ } //Iterate through each of the datasets, and build this into a property of the chart - helpers.each(data.datasets,function(dataset){ + helpers.each(this.data.datasets,function(dataset){ var datasetObject = { label: dataset.label || null, @@ -121,14 +156,22 @@ } datasetObject.points.push(new this.PointClass({ value : dataPoint, - label : data.labels[index], + label : this.data.labels[index], datasetLabel: dataset.label, x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, 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); @@ -168,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++; @@ -311,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){ @@ -326,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 new file mode 100644 index 00000000000..b0313e4c057 --- /dev/null +++ b/src/Chart.Scale.js @@ -0,0 +1,1155 @@ +(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 = { + // The interesting function + fitScalesForChart: function(chartInstance, width, height) { + var xPadding = 10; + var yPadding = 10; + + if (chartInstance) { + var leftScales = helpers.where(chartInstance.scales, function(scaleInstance) { + return scaleInstance.options.position == "left"; + }); + var rightScales = helpers.where(chartInstance.scales, function(scaleInstance) { + return scaleInstance.options.position == "right"; + }); + var topScales = helpers.where(chartInstance.scales, function(scaleInstance) { + return scaleInstance.options.position == "top"; + }); + var bottomScales = helpers.where(chartInstance.scales, function(scaleInstance) { + 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 + // 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; + } + } + + chartWidth -= (2 * xPadding); + chartHeight-= (2 * yPadding); + + // Step 2 + var verticalScaleWidth = (width - chartWidth) / (leftScales.length + rightScales.length); + + // Step 3 + var horizontalScaleHeight = (height - chartHeight) / (topScales.length + bottomScales.length); + + // Step 4; + var minimumScaleSizes = []; + + var verticalScaleMinSizeFunction = function(scaleInstance) { + var minSize = scaleInstance.fit(verticalScaleWidth, chartHeight); + minimumScaleSizes.push({ + horizontal: false, + minSize: minSize, + scale: scaleInstance, + }); + }; + + var horizontalScaleMinSizeFunction = function(scaleInstance) { + var minSize = scaleInstance.fit(chartWidth, horizontalScaleHeight); + minimumScaleSizes.push({ + horizontal: true, + minSize: minSize, + scale: scaleInstance, + }); + }; + + // 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 - (2 * yPadding); + var maxChartWidth = width - (2 * xPadding); + + helpers.each(minimumScaleSizes, function(wrapper) { + if (wrapper.horizontal) { + maxChartHeight -= wrapper.minSize.height; + } else { + maxChartWidth -= wrapper.minSize.width; + } + }); + + // 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 wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) { + return wrapper.scale === scaleInstance; + }); + + if (wrapper) { + scaleInstance.fit(wrapper.minSize.width, maxChartHeight); + } + }; + + var horizontalScaleFitFunction = function(scaleInstance) { + var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) { + return wrapper.scale === scaleInstance; + }); + + if (wrapper) { + scaleInstance.fit(maxChartWidth, wrapper.minSize.width); + } + }; + + helpers.each(leftScales, verticalScaleFitFunction); + helpers.each(rightScales, verticalScaleFitFunction); + helpers.each(topScales, horizontalScaleFitFunction); + helpers.each(bottomScales, horizontalScaleFitFunction); + + // Step 7 + var totalLeftWidth = xPadding; + var totalTopHeight = yPadding; + + // 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 = xPadding; + var top = yPadding; + 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 + 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 { + // 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))); + } + + // 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); + }, + 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.labels.userCallback) { + // If the user provided a callback for label generation, use that as first priority + 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, { + 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; + 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 + // 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; + }, + // 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.display) { + + 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 + 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; + + 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.beginPath(); + + if (this.options.gridLines.drawTicks) { + 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); + } + + // Need to stroke in the loop because we are potentially changing line widths & colours + this.ctx.stroke(); + }, this); + } + + 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 == "right" ? this.left : this.right - 10; + var xTickEnd = this.options.position == "right" ? this.left + 10 : this.right; + + 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; // use boolean to indicate that we only want to do this once + } + + yValue += helpers.aliasPixel(this.ctx.lineWidth); + + // Draw the label area + this.ctx.beginPath(); + + if (this.options.gridLines.drawTicks) { + 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.ctx.stroke(); + }, this); + } + + 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); + + 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"; + }, + 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; + + if (this.options.gridLines.offsetGridLines && includeOffset) { + valueOffset += (valueWidth / 2); + } + + return this.left + Math.round(valueOffset); + } else { + return this.top + (index * (this.height / this.max)); + } + }, + calculateLabelRotation: function(maxHeight) { + //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.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.labelRotation = 0; + + if (this.options.display) { + 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 + // 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.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.options.labels.fontSize / 2 > this.yLabelWidth) { + this.paddingLeft = firstRotated + this.options.labels.fontSize / 2; + } + + this.paddingRight = this.options.labels.fontSize / 2; + + if (sinRotation * originalLabelWidth > maxHeight) { + // go back one step + this.labelRotation--; + break; + } + + this.labelRotation++; + this.labelWidth = cosRotation * originalLabelWidth; + + } + } else { + this.labelWidth = 0; + this.paddingRight = this.padding; + this.paddingLeft = 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) { + 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.display) { + + 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); + + 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 diff --git a/src/Chart.Scatter.js b/src/Chart.Scatter.js new file mode 100644 index 00000000000..240160b7ae7 --- /dev/null +++ b/src/Chart.Scatter.js @@ -0,0 +1,502 @@ +(function() { + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + hoverMode: 'single', + scales: { + xAxes: [{ + scaleType: "linear", // scatter should not use a dataset axis + display: true, + position: "bottom", + 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)", + }, + + // 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 + display: true, + position: "left", + 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", + } + }], + }, + + //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%><%}%>
  • <%}%>
", + + tooltipTemplate: "(<%= dataX %>, <%= dataY %>)", + multiTooltipTemplate: "<%if (datasetLabel){%><%=datasetLabel%>: <%}%>(<%= dataX %>, <%= dataY %>)", + + }; + + + Chart.Type.extend({ + name: "Scatter", + defaults: defaultConfig, + initialize: function() { + //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.events, this.events); + + // Build Scale + this.buildScale(); + 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); + + // Make sure each dataset is bound to an x and a y axis + if (!dataset.xAxisID) { + dataset.xAxisID = this.options.scales.xAxes[0].id; + } + + if (!dataset.yAxisID) { + dataset.yAxisID = this.options.scales.yAxes[0].id; + } + }, 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) { + var xScale = this.scales[this.data.datasets[datasetIndex].xAxisID]; + + helpers.extend(point, { + x: 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]; + }, + events: function(e) { + // If exiting chart + if (e.type == 'mouseout') { + return this; + } + + this.lastActive = this.lastActive || []; + + // Find Active Elements + this.active = function() { + switch (this.options.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) { + var xScale = this.scales[this.data.datasets[datasetIndex].xAxisID]; + var yScale = this.scales[this.data.datasets[datasetIndex].yAxisID]; + + helpers.extend(point, { + x: xScale.getPixelForValue(this.data.datasets[datasetIndex].data[index].x), + y: yScale.getPixelForValue(this.data.datasets[datasetIndex].data[index].y), + 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, + 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() { + var self = this; + + var calculateXRange = function() { + this.min = null; + this.max = null; + + helpers.each(self.data.datasets, function(dataset) { + // Only set the scale range for datasets that actually use this axis + if (dataset.xAxisID === this.id) { + 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); + }; + + var calculateYRange = 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) { + 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); + }; + + // Map of scale ID to scale object so we can lookup later + this.scales = {}; + + helpers.each(this.options.scales.xAxes, function(xAxisOptions) { + var ScaleClass = Chart.scales.getScaleConstructor(xAxisOptions.scaleType); + var scale = new ScaleClass({ + ctx: this.chart.ctx, + options: xAxisOptions, + calculateRange: calculateXRange, + id: xAxisOptions.id, + }); + + this.scales[scale.id] = scale; + }, this); + + 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, + id: yAxisOptions.id, + }); + + this.scales[scale.id] = scale; + }, this); + }, + redraw: function() { + + }, + draw: function(ease) { + + var easingDecimal = ease || 1; + this.clear(); + + // Draw all the scales + helpers.each(this.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);