]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Feature #73 - Horizontal Bar Chart (#2448)
authorpotatopeelings <potato.peelings@gmail.com>
Tue, 3 May 2016 21:45:43 +0000 (07:45 +1000)
committerTanner Linsley <tannerlinsley@gmail.com>
Tue, 3 May 2016 21:45:43 +0000 (16:45 -0500)
* Horizontal bar chart type

* Mentioned horizontal bar chart in bar documentation

* Sample file for horizontal bar chart

* Missing semicolon

* Fix for borderSkipped index

docs/03-Bar-Chart.md
samples/horizontalBar.html [new file with mode: 0644]
src/controllers/controller.bar.js

index d1f04960574b0a4a89edb68001d703fda75066c0..011f1e1c17faf40f7198e33597b804472a3fa10f 100644 (file)
@@ -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 (file)
index 0000000..ae7bdad
--- /dev/null
@@ -0,0 +1,143 @@
+<!doctype html>
+<html>
+
+<head>
+    <title>Horizontal Bar Chart</title>
+    <script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
+    <script src="../dist/Chart.bundle.js"></script>
+    <style>
+    canvas {
+        -moz-user-select: none;
+        -webkit-user-select: none;
+        -ms-user-select: none;
+    }
+    </style>
+</head>
+
+<body>
+    <div id="container" style="width: 75%;">
+        <canvas id="canvas"></canvas>
+    </div>
+    <button id="randomizeData">Randomize Data</button>
+    <button id="addDataset">Add Dataset</button>
+    <button id="removeDataset">Remove Dataset</button>
+    <button id="addData">Add Data</button>
+    <button id="removeData">Remove Data</button>
+    <script>
+        var MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
+
+        var randomScalingFactor = function() {
+            return (Math.random() > 0.5 ? 1.0 : -1.0) * Math.round(Math.random() * 100);
+        };
+        var randomColorFactor = function() {
+            return Math.round(Math.random() * 255);
+        };
+        var randomColor = function() {
+            return 'rgba(' + randomColorFactor() + ',' + randomColorFactor() + ',' + randomColorFactor() + ',.7)';
+        };
+
+        var horizontalBarChartData = {
+            labels: ["January", "February", "March", "April", "May", "June", "July"],
+            datasets: [{
+                label: 'Dataset 1',
+                backgroundColor: "rgba(220,220,220,0.5)",
+                data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()]
+            }, {
+                hidden: true,
+                label: 'Dataset 2',
+                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.myHorizontalBar = new Chart(ctx, {
+                type: 'horizontalBar',
+                data: horizontalBarChartData,
+                options: {
+                    // Elements options apply to all of the options unless overridden in a dataset
+                    // In this case, we are setting the border of each horizontal bar to be 2px wide and green
+                    elements: {
+                        rectangle: {
+                            borderWidth: 2,
+                            borderColor: 'rgb(0, 255, 0)',
+                            borderSkipped: 'left'
+                        }
+                    },
+                    responsive: true,
+                    legend: {
+                        position: 'top',
+                    },
+                    title: {
+                        display: true,
+                        text: 'Chart.js Horizontal Bar Chart'
+                    }
+                }
+            });
+
+        };
+
+        $('#randomizeData').click(function() {
+            var zero = Math.random() < 0.2 ? true : false;
+            $.each(horizontalBarChartData.datasets, function(i, dataset) {
+                dataset.backgroundColor = randomColor();
+                dataset.data = dataset.data.map(function() {
+                    return zero ? 0.0 : randomScalingFactor();
+                });
+
+            });
+            window.myHorizontalBar.update();
+        });
+
+        $('#addDataset').click(function() {
+            var newDataset = {
+                label: 'Dataset ' + horizontalBarChartData.datasets.length,
+                backgroundColor: randomColor(),
+                data: []
+            };
+
+            for (var index = 0; index < horizontalBarChartData.labels.length; ++index) {
+                newDataset.data.push(randomScalingFactor());
+            }
+
+            horizontalBarChartData.datasets.push(newDataset);
+            window.myHorizontalBar.update();
+        });
+
+        $('#addData').click(function() {
+            if (horizontalBarChartData.datasets.length > 0) {
+                var month = MONTHS[horizontalBarChartData.labels.length % MONTHS.length];
+                horizontalBarChartData.labels.push(month);
+
+                for (var index = 0; index < horizontalBarChartData.datasets.length; ++index) {
+                    horizontalBarChartData.datasets[index].data.push(randomScalingFactor());
+                }
+
+                window.myHorizontalBar.update();
+            }
+        });
+
+        $('#removeDataset').click(function() {
+            horizontalBarChartData.datasets.splice(0, 1);
+            window.myHorizontalBar.update();
+        });
+
+        $('#removeData').click(function() {
+            horizontalBarChartData.labels.splice(-1, 1); // remove the label first
+
+            horizontalBarChartData.datasets.forEach(function (dataset, datasetIndex) {
+                dataset.data.pop();
+            });
+
+            window.myHorizontalBar.update();
+        });
+    </script>
+</body>
+
+</html>
index a4fca99dc173393a76292339f7ff9fde742514c8..0af54676765a2ab5304dda000ee9fae12e3c5063 100644 (file)
@@ -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 = {\r
+               hover: {\r
+                       mode: "label"\r
+               },
+
+               scales: {\r
+                       xAxes: [{\r
+                               type: "linear",
+                               position: "bottom"\r
+                       }],\r
+                       yAxes: [{\r
+                               position: "left",
+                               type: "category",
+
+                               // Specific to Horizontal Bar Controller
+                               categoryPercentage: 0.8,
+                               barPercentage: 0.9,
+
+                               // grid line settings
+                               gridLines: {\r
+                                       offsetGridLines: true\r
+                               }\r
+                       }]
+               },\r
+       };
+
+       Chart.controllers.horizontalBar = Chart.controllers.bar.extend({\r
+               updateElement: function updateElement(rectangle, index, reset, numBars) {\r
+                       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);\r
+                       } else if (xScale.min > 0 && xScale.max > 0) {\r
+                               xScalePoint = xScale.getPixelForValue(xScale.min);\r
+                       } else {\r
+                               xScalePoint = xScale.getPixelForValue(0);\r
+                       }
+
+                       helpers.extend(rectangle, {
+                               // Utility
+                               _chart: this.chart.chart,
+                               _xScale: xScale,
+                               _yScale: yScale,
+                               _datasetIndex: this.index,
+                               _index: index,
+
+                               // Desired view properties
+                               _model: {\r
+                                       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)\r
+                               },
+
+                               draw: function () {\r
+
+                                       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) {\r
+                                               topY += halfStroke;
+                                               bottomY -= halfStroke;
+                                               right += halfStroke;\r
+                                       }
+
+                                       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) {\r
+                                               return corners[(startCorner + index) % 4];\r
+                                       }
+
+                                       // 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) {\r
+                                               ctx.stroke();\r
+                                       }\r
+                               },
+
+                               inRange: function (mouseX, mouseY) {\r
+                                       var vm = this._view;
+                                       var inRange = false;
+
+                                       if (vm) {\r
+                                               if (vm.x < vm.base) {\r
+                                                       inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.x && mouseX <= vm.base);\r
+                                               } else {\r
+                                                       inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.base && mouseX <= vm.x);\r
+                                               }\r
+                                       }
+
+                                       return inRange;\r
+                               }\r
+                       });
+
+                       rectangle.pivot();\r
+               },
+
+               calculateBarBase: function (datasetIndex, index) {\r
+                       var meta = this.getMeta();
+                       var xScale = this.getScaleForId(meta.xAxisID);
+                       var yScale = this.getScaleForId(meta.yAxisID);
+
+                       var base = 0;
+
+                       if (xScale.options.stacked) {\r
+
+                               var value = this.chart.data.datasets[datasetIndex].data[index];
+
+                               if (value < 0) {\r
+                                       for (var i = 0; i < datasetIndex; i++) {\r
+                                               var negDS = this.chart.data.datasets[i];
+                                               var negDSMeta = this.chart.getDatasetMeta(i);
+                                               if (negDSMeta.bar && negDSMeta.xAxisID === xScale.id && this.chart.isDatasetVisible(i)) {\r
+                                                       base += negDS.data[index] < 0 ? negDS.data[index] : 0;\r
+                                               }\r
+                                       }\r
+                               } else {\r
+                                       for (var j = 0; j < datasetIndex; j++) {\r
+                                               var posDS = this.chart.data.datasets[j];
+                                               var posDSMeta = this.chart.getDatasetMeta(j);
+                                               if (posDSMeta.bar && posDSMeta.xAxisID === xScale.id && this.chart.isDatasetVisible(j)) {\r
+                                                       base += posDS.data[index] > 0 ? posDS.data[index] : 0;\r
+                                               }\r
+                                       }\r
+                               }
+
+                               return xScale.getPixelForValue(base);\r
+                       }
+
+                       base = xScale.getPixelForValue(xScale.min);
+
+                       if (xScale.beginAtZero || ((xScale.min <= 0 && xScale.max >= 0) || (xScale.min >= 0 && xScale.max <= 0))) {\r
+                               base = xScale.getPixelForValue(0, 0);\r
+                       } else if (xScale.min < 0 && xScale.max < 0) {
+                               // All values are negative. Use the right as the base
+                               base = xScale.getPixelForValue(xScale.max);\r
+                       }
+
+                       return base;\r
+               },
+
+               getRuler: function () {\r
+                       var meta = this.getMeta();
+                       var xScale = this.getScaleForId(meta.xAxisID);
+                       var yScale = this.getScaleForId(meta.yAxisID);
+                       var datasetCount = this.getBarCount();
+
+                       var tickHeight = (function () {\r
+                               var min = yScale.getPixelForTick(1) - yScale.getPixelForTick(0);
+                               for (var i = 2; i < this.getDataset().data.length; i++) {\r
+                                       min = Math.min(yScale.getPixelForTick(i) - yScale.getPixelForTick(i - 1), min);\r
+                               }
+                               return min;\r
+                       }).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) {\r
+                               var perc = yScale.ticks.length / this.chart.data.labels.length;
+                               fullBarHeight = fullBarHeight * perc;\r
+                       }
+
+                       var barHeight = fullBarHeight * yScale.options.barPercentage;
+                       var barSpacing = fullBarHeight - (fullBarHeight * yScale.options.barPercentage);
+
+                       return {\r
+                               datasetCount: datasetCount,
+                               tickHeight: tickHeight,
+                               categoryHeight: categoryHeight,
+                               categorySpacing: categorySpacing,
+                               fullBarHeight: fullBarHeight,
+                               barHeight: barHeight,
+                               barSpacing: barSpacing,\r
+                       };\r
+               },
+
+               calculateBarHeight: function () {\r
+                       var yScale = this.getScaleForId(this.getMeta().yAxisID);
+                       var ruler = this.getRuler();
+                       return yScale.options.stacked ? ruler.categoryHeight : ruler.barHeight;\r
+               },
+
+               calculateBarX: function (index, datasetIndex) {\r
+                       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) {\r
+\r
+                               var sumPos = 0,
+                                       sumNeg = 0;
+
+                               for (var i = 0; i < datasetIndex; i++) {\r
+                                       var ds = this.chart.data.datasets[i];
+                                       var dsMeta = this.chart.getDatasetMeta(i);
+                                       if (dsMeta.bar && dsMeta.xAxisID === xScale.id && this.chart.isDatasetVisible(i)) {\r
+                                               if (ds.data[index] < 0) {\r
+                                                       sumNeg += ds.data[index] || 0;\r
+                                               } else {\r
+                                                       sumPos += ds.data[index] || 0;\r
+                                               }\r
+                                       }\r
+                               }
+
+                               if (value < 0) {\r
+                                       return xScale.getPixelForValue(sumNeg + value);\r
+                               } else {\r
+                                       return xScale.getPixelForValue(sumPos + value);\r
+                               }
+                       }
+
+                       return xScale.getPixelForValue(value);\r
+               },
+
+               calculateBarY: function (index, datasetIndex) {\r
+                       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) {\r
+                               return topTick + (ruler.categoryHeight / 2) + ruler.categorySpacing;\r
+                       }
+
+                       return topTick +
+                               (ruler.barHeight / 2) +
+                               ruler.categorySpacing +
+                               (ruler.barHeight * barIndex) +
+                               (ruler.barSpacing / 2) +
+                               (ruler.barSpacing * barIndex);\r
+               }\r
+       });
 };