From 6b0e609935cb67cf32c3c8c94fe278f9314d9ce4 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Tue, 2 Jun 2015 16:48:39 -0600 Subject: [PATCH] Bar Chart Stacked nestedConfig --- samples/bar.html | 10 +- src/Chart.Bar.js | 434 ++++++++++++++++++++++------------------------ src/Chart.Core.js | 12 +- 3 files changed, 221 insertions(+), 235 deletions(-) diff --git a/samples/bar.html b/samples/bar.html index d9a5dc4d1..86169d164 100644 --- a/samples/bar.html +++ b/samples/bar.html @@ -34,21 +34,13 @@ label: 'Dataset 3', backgroundColor: "rgba(151,187,205,0.5)", data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()] - }, { - label: 'Dataset 4', - backgroundColor: "rgba(151,187,205,0.5)", - data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()] - }, { - label: 'Dataset 3', - backgroundColor: "rgba(151,187,205,0.5)", - data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()] }] }; window.onload = function() { var ctx = document.getElementById("canvas").getContext("2d"); window.myBar = new Chart(ctx).Bar({ - data: barChartData, + data: barChartData, options: { responsive: true, hoverMode: 'label', diff --git a/src/Chart.Bar.js b/src/Chart.Bar.js index 39482b640..32f67e8f4 100644 --- a/src/Chart.Bar.js +++ b/src/Chart.Bar.js @@ -7,13 +7,20 @@ var defaultConfig = { + + stacked: false, + + hover: { + mode: "label" + }, + 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, @@ -46,7 +53,7 @@ display: true, position: "left", id: "y-axis-1", - + // grid line settings gridLines: { show: true, @@ -75,20 +82,6 @@ }], }, - bars: { - //Number - Pixel width of the bar border - borderWidth: 2, - - //Number - Spacing between each of the X value sets - valueSpacing: 5, - - //Number - Spacing between data sets within X values - datasetSpacing: 1, - }, - - //String - A legend template - legendTemplate: "" - }; @@ -96,22 +89,23 @@ name: "Bar", defaults: defaultConfig, initialize: function() { - // Events - 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, - }); + var _this = this; + + // Events + helpers.bindEvents(this, this.options.events, this.events); //Create a new bar for each piece of data helpers.each(this.data.datasets, function(dataset, datasetIndex) { dataset.metaData = []; helpers.each(dataset.data, function(dataPoint, index) { - dataset.metaData.push(new this.BarClass()); + dataset.metaData.push(new Chart.Rectangle({ + _chart: this.chart, + _datasetIndex: datasetIndex, + })); }, this); - // The bar chart only supports a single x axis because the x axis is always a dataset axis + // 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) { @@ -121,24 +115,6 @@ // 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: 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, - }); - // Copy to view model - bar.save(); - }, this); // Create tooltip instance exclusively for this chart with some defaults. this.tooltip = new Chart.Tooltip({ @@ -150,162 +126,48 @@ // Update the chart with the latest data. this.update(); }, - onHover: function(e) { - - - // If exiting chart - if (e.type == 'mouseout') { - return this; - } - - this.lastActive = this.lastActive || []; - - // Find Active Elements - this.active = function() { - switch (this.options.hover.mode) { - case 'single': - return this.getElementAtEvent(e); - case 'label': - return this.getElementsAtEvent(e); - case 'dataset': - return this.getDatasetAtEvent(e); - default: - return e; - } - }.call(this); - - // On Hover hook - if (this.options.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.hover.mode) { - case 'single': - this.lastActive[0].backgroundColor = this.data.datasets[this.lastActive[0]._datasetIndex].backgroundColor; - this.lastActive[0].borderColor = this.data.datasets[this.lastActive[0]._datasetIndex].borderColor; - break; - case 'label': - for (var i = 0; i < this.lastActive.length; i++) { - this.lastActive[i].backgroundColor = this.data.datasets[this.lastActive[i]._datasetIndex].backgroundColor; - this.lastActive[i].borderColor = this.data.datasets[this.lastActive[i]._datasetIndex].borderColor; - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - // Built in hover styling - if (this.active.length && this.options.hover.mode) { - switch (this.options.hover.mode) { - case 'single': - this.active[0].backgroundColor = this.data.datasets[this.active[0]._datasetIndex].hoverBackgroundColor || helpers.color(this.active[0].backgroundColor).saturate(0.8).darken(0.2).rgbString(); - this.active[0].borderColor = this.data.datasets[this.active[0]._datasetIndex].hoverBorderColor || helpers.color(this.active[0].borderColor).saturate(0.8).darken(0.2).rgbString(); - 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.8).darken(0.2).rgbString(); - this.active[i].borderColor = this.data.datasets[this.active[i]._datasetIndex].hoverBorderColor || helpers.color(this.active[i].borderColor).saturate(0.8).darken(0.2).rgbString(); - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - - // Built in Tooltips - if (this.options.tooltips.enabled) { - - // The usual updates - this.tooltip.initialize(); - - // Active - if (this.active.length) { - helpers.extend(this.tooltip, { - opacity: 1, - _active: this.active, - }); - - this.tooltip.update(); - } else { - // Inactive - helpers.extend(this.tooltip, { - opacity: 0, - }); - } - } - - - this.tooltip.pivot(); - - // Hover animations - 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() { // 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); - + // Update the points 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: 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, - borderWidth: this.data.datasets[datasetIndex].borderWidth, - backgroundColor: this.data.datasets[datasetIndex].backgroundColor, + // Utility + _chart: this.chart, + _xScale: xScale, + _yScale: yScale, _datasetIndex: datasetIndex, _index: index, - }); + // Desired view properties + _model: { + x: xScale.calculateBarX(this.data.datasets.length, datasetIndex, index), + y: yScale.calculateBarY(datasetIndex, index), + + // Appearance + base: yScale.calculateBarBase(datasetIndex, index), + width: xScale.calculateBarWidth(this.data.datasets.length), + backgroundColor: bar.custom && bar.custom.backgroundColor ? bar.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].backgroundColor, index, this.options.elements.bar.backgroundColor), + borderColor: bar.custom && bar.custom.borderColor ? bar.custom.borderColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].borderColor, index, this.options.elements.bar.borderColor), + borderWidth: bar.custom && bar.custom.borderWidth ? bar.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].borderWidth, index, this.options.elements.bar.borderWidth), + + // Tooltip + label: this.data.labels[index], + datasetLabel: this.data.datasets[datasetIndex].label, + }, + }); 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() { @@ -338,6 +200,8 @@ var values = positiveValues.concat(negativeValues); this.min = helpers.min(values); this.max = helpers.max(values); + + console.log(this.min, this.max); } else { helpers.each(self.data.datasets, function(dataset) { if (dataset.yAxisID === this.id) { @@ -347,7 +211,7 @@ } else if (value < this.min) { this.min = value; } - + if (this.max === null) { this.max = value; } else if (value > this.max) { @@ -374,11 +238,11 @@ this.max = this.labels.length; }, calculateBaseWidth: function() { - return (this.getPixelForValue(null, 1, true) - this.getPixelForValue(null, 0, true)) - (2 * self.options.bars.valueSpacing); + return (this.getPixelForValue(null, 1, true) - this.getPixelForValue(null, 0, true)) - (2 * self.options.elements.bar.valueSpacing); }, 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.bars.datasetSpacing); + var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * self.options.elements.bar.datasetSpacing); if (self.options.stacked) { return baseWidth; @@ -394,7 +258,7 @@ return xAbsolute + barWidth / 2; } - return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * self.options.bars.datasetSpacing) + barWidth / 2; + return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * self.options.elements.bar.datasetSpacing) + barWidth / 2; }, }); this.scales[xScale.id] = xScale; @@ -410,22 +274,25 @@ var base = 0; if (self.options.stacked) { - var bar = self.data.datasets[datasetIndex].metaData[index]; - if (bar.value < 0) { + var value = self.data.datasets[datasetIndex].data[index]; + + if (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; + base += self.data.datasets[i].data[index] < 0 ? self.data.datasets[i].data[index] : 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; + for (var j = 0; j < datasetIndex; j++) { + if (self.data.datasets[j].yAxisID === this.id) { + base += self.data.datasets[j].data[index] > 0 ? self.data.datasets[j].data[index] : 0; } } } + console.log(base); + return this.getPixelForValue(base); } @@ -442,7 +309,9 @@ return base; }, - calculateBarY: function(datasets, datasetIndex, barIndex, value) { + calculateBarY: function(datasetIndex, index) { + + var value = self.data.datasets[datasetIndex].data[index]; if (self.options.stacked) { @@ -450,10 +319,10 @@ sumNeg = 0; for (var i = 0; i < datasetIndex; i++) { - if (datasets[i].metaData[barIndex].value < 0) { - sumNeg += datasets[i].metaData[barIndex].value || 0; + if (self.data.datasets[i].data[index] < 0) { + sumNeg += self.data.datasets[i].data[index] || 0; } else { - sumPos += datasets[i].metaData[barIndex].value || 0; + sumPos += self.data.datasets[i].data[index] || 0; } } @@ -463,52 +332,27 @@ return this.getPixelForValue(sumPos + value); } - return this.getPixelForValue(0); + return this.getPixelForValue(value); } var offset = 0; - for (i = datasetIndex; i < datasets.length; i++) { - if (i === datasetIndex && value) { + for (j = datasetIndex; j < datasets.length; j++) { + if (j === datasetIndex && value) { offset += value; } else { - offset = offset + (datasets[i].metaData[barIndex].value); + offset = offset + value; } } return this.getPixelForValue(value); }, - - calculateBaseHeight: function() { - return (this.getPixelForValue(1) - this.getPixelForValue(0)); - }, id: yAxisOptions.id, }); 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() { - 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 - }); - }); - this.render(); - }, draw: function(ease) { var easingDecimal = ease || 1; @@ -526,7 +370,149 @@ // Finally draw the tooltip this.tooltip.transition(easingDecimal).draw(); - } + }, + events: function(e) { + + + // If exiting chart + if (e.type == 'mouseout') { + return this; + } + + this.lastActive = this.lastActive || []; + + // Find Active Elements + this.active = function() { + switch (this.options.hover.mode) { + case 'single': + return this.getElementAtEvent(e); + case 'label': + return this.getElementsAtEvent(e); + case 'dataset': + return this.getDatasetAtEvent(e); + default: + return e; + } + }.call(this); + + // On Hover hook + if (this.options.hover.onHover) { + this.options.hover.onHover.call(this, this.active); + } + + if (e.type == 'mouseup' || e.type == 'click') { + if (this.options.onClick) { + this.options.onClick.call(this, e, this.active); + } + } + + var dataset; + var index; + // Remove styling for last active (even if it may still be active) + if (this.lastActive.length) { + switch (this.options.hover.mode) { + case 'single': + dataset = this.data.datasets[this.lastActive[0]._datasetIndex]; + index = this.lastActive[0]._index; + + this.lastActive[0]._model.backgroundColor = this.lastActive[0].custom && this.lastActive[0].custom.backgroundColor ? this.lastActive[0].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, this.options.elements.bar.backgroundColor); + this.lastActive[0]._model.borderColor = this.lastActive[0].custom && this.lastActive[0].custom.borderColor ? this.lastActive[0].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, this.options.elements.bar.borderColor); + this.lastActive[0]._model.borderWidth = this.lastActive[0].custom && this.lastActive[0].custom.borderWidth ? this.lastActive[0].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.options.elements.bar.borderWidth); + break; + case 'label': + for (var i = 0; i < this.lastActive.length; i++) { + dataset = this.data.datasets[this.lastActive[i]._datasetIndex]; + index = this.lastActive[i]._index; + + this.lastActive[i]._model.backgroundColor = this.lastActive[i].custom && this.lastActive[i].custom.backgroundColor ? this.lastActive[i].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, this.options.elements.bar.backgroundColor); + this.lastActive[i]._model.borderColor = this.lastActive[i].custom && this.lastActive[i].custom.borderColor ? this.lastActive[i].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, this.options.elements.bar.borderColor); + this.lastActive[i]._model.borderWidth = this.lastActive[i].custom && this.lastActive[i].custom.borderWidth ? this.lastActive[i].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.options.elements.bar.borderWidth); + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + // Built in hover styling + if (this.active.length && this.options.hover.mode) { + switch (this.options.hover.mode) { + case 'single': + dataset = this.data.datasets[this.active[0]._datasetIndex]; + index = this.active[0]._index; + + this.active[0]._model.backgroundColor = this.active[0].custom && this.active[0].custom.hoverBackgroundColor ? this.active[0].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.color(this.active[0]._model.backgroundColor).saturate(0.5).darken(0.35).rgbString()); + this.active[0]._model.borderColor = this.active[0].custom && this.active[0].custom.hoverBorderColor ? this.active[0].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.color(this.active[0]._model.borderColor).saturate(0.5).darken(0.35).rgbString()); + this.active[0]._model.borderWidth = this.active[0].custom && this.active[0].custom.hoverBorderWidth ? this.active[0].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.active[0]._model.borderWidth); + break; + case 'label': + for (var i = 0; i < this.active.length; i++) { + dataset = this.data.datasets[this.active[i]._datasetIndex]; + index = this.active[i]._index; + + this.active[i]._model.backgroundColor = this.active[i].custom && this.active[i].custom.hoverBackgroundColor ? this.active[i].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.color(this.active[i]._model.backgroundColor).saturate(0.5).darken(0.35).rgbString()); + this.active[i]._model.borderColor = this.active[i].custom && this.active[i].custom.hoverBorderColor ? this.active[i].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.color(this.active[i]._model.borderColor).saturate(0.5).darken(0.35).rgbString()); + this.active[i]._model.borderWidth = this.active[i].custom && this.active[i].custom.hoverBorderWidth ? this.active[i].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.active[i]._model.borderWidth); + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + + // Built in Tooltips + if (this.options.tooltips.enabled) { + + // The usual updates + this.tooltip.initialize(); + + // Active + if (this.active.length) { + this.tooltip._model.opacity = 1; + + helpers.extend(this.tooltip, { + _active: this.active, + }); + + this.tooltip.update(); + } else { + // Inactive + this.tooltip._model.opacity = 0; + } + } + + + this.tooltip.pivot(); + + // Hover animations + 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; + }, }); diff --git a/src/Chart.Core.js b/src/Chart.Core.js index a38ffdc44..ca8ec0105 100755 --- a/src/Chart.Core.js +++ b/src/Chart.Core.js @@ -65,6 +65,7 @@ events: ["mousemove", "mouseout", "click", "touchstart", "touchmove", "touchend"], hover: { onHover: null, + mode: 'single', animationDuration: 400, }, onClick: null, @@ -127,6 +128,13 @@ hoverRadius: 4, hoverBorderWidth: 2, }, + bar: { + backgroundColor: defaultColor, + borderWidth: 2, + borderColor: defaultColor, + valueSpacing: 5, + datasetSpacing: 1, + }, } }, }; @@ -1462,9 +1470,9 @@ draw: function() { var vm = this._view; + var ctx = this._chart.ctx; - var ctx = this.ctx, - halfWidth = vm.width / 2, + var halfWidth = vm.width / 2, leftX = vm.x - halfWidth, rightX = vm.x + halfWidth, top = vm.base - (vm.base - vm.y),