From: potatopeelings Date: Tue, 3 May 2016 21:45:43 +0000 (+1000) Subject: Feature #73 - Horizontal Bar Chart (#2448) X-Git-Tag: 2.1.0~1 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=c2cc3fbb4c550ae89091a54a00e224524eb479d0;p=thirdparty%2FChart.js.git Feature #73 - Horizontal Bar Chart (#2448) * Horizontal bar chart type * Mentioned horizontal bar chart in bar documentation * Sample file for horizontal bar chart * Missing semicolon * Fix for borderSkipped index --- diff --git a/docs/03-Bar-Chart.md b/docs/03-Bar-Chart.md index d1f049605..011f1e1c1 100644 --- a/docs/03-Bar-Chart.md +++ b/docs/03-Bar-Chart.md @@ -21,6 +21,16 @@ var myBarChart = new Chart(ctx, { }); ``` +Or if you want horizontal bars. + +```javascript +var myBarChart = new Chart(ctx, { + type: 'horizontalBar', + data: data, + options: options +}); +``` + ### Data structure The following options can be included in a bar chart dataset to configure options for that specific dataset. @@ -113,7 +123,16 @@ new Chart(ctx, { // for both x and y axes. ``` -We can also change these defaults values for each Bar type that is created, this object is available at `Chart.defaults.bar`. +We can also change these defaults values for each Bar type that is created, this object is available at `Chart.defaults.bar`. For horizontal bars, this object is available at `Chart.defaults.horizontalBar`. + +The default options for horizontal bar charts are defined in `Chart.defaults.horizontalBar` and are same as those of the bar chart, but with `xAxes` and `yAxes` swapped and the following additional options. + +Name | Type | Default | Description +--- |:---:| --- | --- +*Options for xAxes* | | | +position | String | "bottom" | +*Options for yAxes* | | | +position | String | "left" | ### barPercentage vs categoryPercentage diff --git a/samples/horizontalBar.html b/samples/horizontalBar.html new file mode 100644 index 000000000..ae7bdad42 --- /dev/null +++ b/samples/horizontalBar.html @@ -0,0 +1,143 @@ + + + + + Horizontal Bar Chart + + + + + + +
+ +
+ + + + + + + + + diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index a4fca99dc..0af546767 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -314,4 +314,294 @@ module.exports = function(Chart) { } }); + + + // including horizontalBar in the bar file, instead of a file of its own + // it extends bar (like pie extends doughnut) + Chart.defaults.horizontalBar = { + hover: { + mode: "label" + }, + + scales: { + xAxes: [{ + type: "linear", + position: "bottom" + }], + yAxes: [{ + position: "left", + type: "category", + + // Specific to Horizontal Bar Controller + categoryPercentage: 0.8, + barPercentage: 0.9, + + // grid line settings + gridLines: { + offsetGridLines: true + } + }] + }, + }; + + Chart.controllers.horizontalBar = Chart.controllers.bar.extend({ + updateElement: function updateElement(rectangle, index, reset, numBars) { + var meta = this.getMeta(); + var xScale = this.getScaleForId(meta.xAxisID); + var yScale = this.getScaleForId(meta.yAxisID); + + var xScalePoint; + + if (xScale.min < 0 && xScale.max < 0) { + // all less than 0. use the right + xScalePoint = xScale.getPixelForValue(xScale.max); + } else if (xScale.min > 0 && xScale.max > 0) { + xScalePoint = xScale.getPixelForValue(xScale.min); + } else { + xScalePoint = xScale.getPixelForValue(0); + } + + helpers.extend(rectangle, { + // Utility + _chart: this.chart.chart, + _xScale: xScale, + _yScale: yScale, + _datasetIndex: this.index, + _index: index, + + // Desired view properties + _model: { + x: reset ? xScalePoint : this.calculateBarX(index, this.index), + y: this.calculateBarY(index, this.index), + + // Tooltip + label: this.chart.data.labels[index], + datasetLabel: this.getDataset().label, + + // Appearance + base: reset ? xScalePoint : this.calculateBarBase(this.index, index), + height: this.calculateBarHeight(numBars), + backgroundColor: rectangle.custom && rectangle.custom.backgroundColor ? rectangle.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.rectangle.backgroundColor), + borderSkipped: rectangle.custom && rectangle.custom.borderSkipped ? rectangle.custom.borderSkipped : this.chart.options.elements.rectangle.borderSkipped, + borderColor: rectangle.custom && rectangle.custom.borderColor ? rectangle.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.rectangle.borderColor), + borderWidth: rectangle.custom && rectangle.custom.borderWidth ? rectangle.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.rectangle.borderWidth) + }, + + draw: function () { + + var ctx = this._chart.ctx; + var vm = this._view; + + var halfHeight = vm.height / 2, + topY = vm.y - halfHeight, + bottomY = vm.y + halfHeight, + right = vm.base - (vm.base - vm.x), + halfStroke = vm.borderWidth / 2; + + // Canvas doesn't allow us to stroke inside the width so we can + // adjust the sizes to fit if we're setting a stroke on the line + if (vm.borderWidth) { + topY += halfStroke; + bottomY -= halfStroke; + right += halfStroke; + } + + ctx.beginPath(); + + ctx.fillStyle = vm.backgroundColor; + ctx.strokeStyle = vm.borderColor; + ctx.lineWidth = vm.borderWidth; + + // Corner points, from bottom-left to bottom-right clockwise + // | 1 2 | + // | 0 3 | + var corners = [ + [vm.base, bottomY], + [vm.base, topY], + [right, topY], + [right, bottomY] + ]; + + // Find first (starting) corner with fallback to 'bottom' + var borders = ['bottom', 'left', 'top', 'right']; + var startCorner = borders.indexOf(vm.borderSkipped, 0); + if (startCorner === -1) + startCorner = 0; + + function cornerAt(index) { + return corners[(startCorner + index) % 4]; + } + + // Draw rectangle from 'startCorner' + ctx.moveTo.apply(ctx, cornerAt(0)); + for (var i = 1; i < 4; i++) + ctx.lineTo.apply(ctx, cornerAt(i)); + + ctx.fill(); + if (vm.borderWidth) { + ctx.stroke(); + } + }, + + inRange: function (mouseX, mouseY) { + var vm = this._view; + var inRange = false; + + if (vm) { + if (vm.x < vm.base) { + inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.x && mouseX <= vm.base); + } else { + inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.base && mouseX <= vm.x); + } + } + + return inRange; + } + }); + + rectangle.pivot(); + }, + + calculateBarBase: function (datasetIndex, index) { + var meta = this.getMeta(); + var xScale = this.getScaleForId(meta.xAxisID); + var yScale = this.getScaleForId(meta.yAxisID); + + var base = 0; + + if (xScale.options.stacked) { + + var value = this.chart.data.datasets[datasetIndex].data[index]; + + if (value < 0) { + for (var i = 0; i < datasetIndex; i++) { + var negDS = this.chart.data.datasets[i]; + var negDSMeta = this.chart.getDatasetMeta(i); + if (negDSMeta.bar && negDSMeta.xAxisID === xScale.id && this.chart.isDatasetVisible(i)) { + base += negDS.data[index] < 0 ? negDS.data[index] : 0; + } + } + } else { + for (var j = 0; j < datasetIndex; j++) { + var posDS = this.chart.data.datasets[j]; + var posDSMeta = this.chart.getDatasetMeta(j); + if (posDSMeta.bar && posDSMeta.xAxisID === xScale.id && this.chart.isDatasetVisible(j)) { + base += posDS.data[index] > 0 ? posDS.data[index] : 0; + } + } + } + + return xScale.getPixelForValue(base); + } + + base = xScale.getPixelForValue(xScale.min); + + if (xScale.beginAtZero || ((xScale.min <= 0 && xScale.max >= 0) || (xScale.min >= 0 && xScale.max <= 0))) { + base = xScale.getPixelForValue(0, 0); + } else if (xScale.min < 0 && xScale.max < 0) { + // All values are negative. Use the right as the base + base = xScale.getPixelForValue(xScale.max); + } + + return base; + }, + + getRuler: function () { + var meta = this.getMeta(); + var xScale = this.getScaleForId(meta.xAxisID); + var yScale = this.getScaleForId(meta.yAxisID); + var datasetCount = this.getBarCount(); + + var tickHeight = (function () { + var min = yScale.getPixelForTick(1) - yScale.getPixelForTick(0); + for (var i = 2; i < this.getDataset().data.length; i++) { + min = Math.min(yScale.getPixelForTick(i) - yScale.getPixelForTick(i - 1), min); + } + return min; + }).call(this); + var categoryHeight = tickHeight * yScale.options.categoryPercentage; + var categorySpacing = (tickHeight - (tickHeight * yScale.options.categoryPercentage)) / 2; + var fullBarHeight = categoryHeight / datasetCount; + + if (yScale.ticks.length !== this.chart.data.labels.length) { + var perc = yScale.ticks.length / this.chart.data.labels.length; + fullBarHeight = fullBarHeight * perc; + } + + var barHeight = fullBarHeight * yScale.options.barPercentage; + var barSpacing = fullBarHeight - (fullBarHeight * yScale.options.barPercentage); + + return { + datasetCount: datasetCount, + tickHeight: tickHeight, + categoryHeight: categoryHeight, + categorySpacing: categorySpacing, + fullBarHeight: fullBarHeight, + barHeight: barHeight, + barSpacing: barSpacing, + }; + }, + + calculateBarHeight: function () { + var yScale = this.getScaleForId(this.getMeta().yAxisID); + var ruler = this.getRuler(); + return yScale.options.stacked ? ruler.categoryHeight : ruler.barHeight; + }, + + calculateBarX: function (index, datasetIndex) { + var meta = this.getMeta(); + var xScale = this.getScaleForId(meta.xAxisID); + var yScale = this.getScaleForId(meta.yAxisID); + + var value = this.getDataset().data[index]; + + if (xScale.options.stacked) { + + var sumPos = 0, + sumNeg = 0; + + for (var i = 0; i < datasetIndex; i++) { + var ds = this.chart.data.datasets[i]; + var dsMeta = this.chart.getDatasetMeta(i); + if (dsMeta.bar && dsMeta.xAxisID === xScale.id && this.chart.isDatasetVisible(i)) { + if (ds.data[index] < 0) { + sumNeg += ds.data[index] || 0; + } else { + sumPos += ds.data[index] || 0; + } + } + } + + if (value < 0) { + return xScale.getPixelForValue(sumNeg + value); + } else { + return xScale.getPixelForValue(sumPos + value); + } + } + + return xScale.getPixelForValue(value); + }, + + calculateBarY: function (index, datasetIndex) { + var meta = this.getMeta(); + var yScale = this.getScaleForId(meta.yAxisID); + var xScale = this.getScaleForId(meta.xAxisID); + var barIndex = this.getBarIndex(datasetIndex); + + var ruler = this.getRuler(); + var topTick = yScale.getPixelForValue(null, index, datasetIndex, this.chart.isCombo); + topTick -= this.chart.isCombo ? (ruler.tickHeight / 2) : 0; + + if (yScale.options.stacked) { + return topTick + (ruler.categoryHeight / 2) + ruler.categorySpacing; + } + + return topTick + + (ruler.barHeight / 2) + + ruler.categorySpacing + + (ruler.barHeight * barIndex) + + (ruler.barSpacing / 2) + + (ruler.barSpacing * barIndex); + } + }); };