How to create rounded bars for Bar Chart.js v2?
Trying to round the bars on bar chart as found in this post that works as displayed in the jsFiddle provided. This is for version 1.
In the chart that I am using, it fails to load for the reference to extend
in Chart.types.Bar.extend
crashes the script.
If I use the default option, the chart loads no problems. I had to place the Chart.types.Bar.extend
at the end for the default option to load correctly. Run and View this in Full Screen.
I tried implementing this with my version of Chart.js 2.4.0.
Chrome reports:
Uncaught TypeError: Cannot read property 'extend' of undefined chart.js
This code will not even run here. Why is this occurring? Could someone please assist me.
This code works with an older version of Chart.js 1.0. Could anyone please further display how this could work with version Chart.js 2.0? Thank you.
$(document).ready(function(){
var myBarChart1 = new Chart($('#appBarChart2_NoRound'), {
type: 'bar',
data: dataBar2,
options: optionsBar
});
var ctx = $("#appBarChart2").getContext("2d");
var myBarChart2 = new Chart(ctx).BarAlt(dataBarAlt2, {
// 0 (flat) to 1 (more curvy)
curvature: 1
});
});
var dataBarAlt2 = {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [
{
fillColor: "#1A9BFC",
strokeColor: "#1A9BFC",
data: [65, 59, 80, 81, 56, 55, 40],
}
]
};
var dataBar2 = {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [
{
label: "My First dataset",
backgroundColor: '#1A9BFC',
borderColor:'#1A9BFC',
borderWidth: 1,
data: [65, 59, 80, 81, 56, 55, 40],
}
]
};
var optionsBar =
{
scales: {
xAxes: [{
stacked: true,
barThickness: 20,
gridLines:{
display:false,
}
// barPercentage:0.5,
}],
yAxes: [{
stacked: true,
// barPercentage:0.5,
}]
},
legend: {
display: false,
// position: 'left'
}
};
Chart.types.Bar.extend({
name: "BarAlt",
initialize: function (data) {
Chart.types.Bar.prototype.initialize.apply(this, arguments);
if (this.options.curvature !== undefined && this.options.curvature <= 1) {
var rectangleDraw = this.datasets[0].bars[0].draw;
var self = this;
var radius = this.datasets[0].bars[0].width * this.options.curvature * 0.5;
// override the rectangle draw with ours
this.datasets.forEach(function (dataset) {
dataset.bars.forEach(function (bar) {
bar.draw = function () {
// draw the original bar a little down (so that our curve brings it to its original position)
var y = bar.y;
// the min is required so animation does not start from below the axes
bar.y = Math.min(bar.y + radius, self.scale.endPoint - 1);
// adjust the bar radius depending on how much of a curve we can draw
var barRadius = (bar.y - y);
rectangleDraw.apply(bar, arguments);
// draw a rounded rectangle on top
Chart.helpers.drawRoundedRectangle(self.chart.ctx, bar.x - bar.width / 2, bar.y - barRadius + 1, bar.width, bar.height, barRadius);
ctx.fill();
// restore the y value
bar.y = y;
}
})
})
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
<p>Bar Chart - Working</p>
<canvas id="appBarChart2_NoRound" height="100" >
</div>
<div>
<p>Rounded Bar Chart - Not Working</p>
<canvas id="appBarChart2" height="100" >
</div>
The code that you were trying to use is actually for chart.js v1 and, as you discovered, does not work for chart.js v2 (which is almost a full chart.js re-write).
To achieve the same results in chart.js v2, you need to extend Chart.elements.Rectangle
and overwrite it's draw
method in order to paint the rounded top. There is already a chart.js helper method that will draw a rounded rectangle (Chart.helpers.drawRoundedRectangle
), so we will modify it slightly and create a new helper method that will only draw a rounded top (instead of all sides).
// draws a rectangle with a rounded top
Chart.helpers.drawRoundedTopRectangle = function(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
// top right corner
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
// bottom right corner
ctx.lineTo(x + width, y + height);
// bottom left corner
ctx.lineTo(x, y + height);
// top left
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
};
Chart.elements.RoundedTopRectangle = Chart.elements.Rectangle.extend({
draw: function() {
var ctx = this._chart.ctx;
var vm = this._view;
var left, right, top, bottom, signX, signY, borderSkipped;
var borderWidth = vm.borderWidth;
if (!vm.horizontal) {
// bar
left = vm.x - vm.width / 2;
right = vm.x + vm.width / 2;
top = vm.y;
bottom = vm.base;
signX = 1;
signY = bottom > top? 1: -1;
borderSkipped = vm.borderSkipped || 'bottom';
} else {
// horizontal bar
left = vm.base;
right = vm.x;
top = vm.y - vm.height / 2;
bottom = vm.y + vm.height / 2;
signX = right > left? 1: -1;
signY = 1;
borderSkipped = vm.borderSkipped || 'left';
}
// 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 (borderWidth) {
// borderWidth shold be less than bar width and bar height.
var barSize = Math.min(Math.abs(left - right), Math.abs(top - bottom));
borderWidth = borderWidth > barSize? barSize: borderWidth;
var halfStroke = borderWidth / 2;
// Adjust borderWidth when bar top position is near vm.base(zero).
var borderLeft = left + (borderSkipped !== 'left'? halfStroke * signX: 0);
var borderRight = right + (borderSkipped !== 'right'? -halfStroke * signX: 0);
var borderTop = top + (borderSkipped !== 'top'? halfStroke * signY: 0);
var borderBottom = bottom + (borderSkipped !== 'bottom'? -halfStroke * signY: 0);
// not become a vertical line?
if (borderLeft !== borderRight) {
top = borderTop;
bottom = borderBottom;
}
// not become a horizontal line?
if (borderTop !== borderBottom) {
left = borderLeft;
right = borderRight;
}
}
// calculate the bar width and roundess
var barWidth = Math.abs(left - right);
var roundness = this._chart.config.options.barRoundness || 0.5;
var radius = barWidth * roundness * 0.5;
// keep track of the original top of the bar
var prevTop = top;
// move the top down so there is room to draw the rounded top
top = prevTop + radius;
var barRadius = top - prevTop;
ctx.beginPath();
ctx.fillStyle = vm.backgroundColor;
ctx.strokeStyle = vm.borderColor;
ctx.lineWidth = borderWidth;
// draw the rounded top rectangle
Chart.helpers.drawRoundedTopRectangle(ctx, left, (top - barRadius + 1), barWidth, bottom - prevTop, barRadius);
ctx.fill();
if (borderWidth) {
ctx.stroke();
}
// restore the original top value so tooltips and scales still work
top = prevTop;
},
});
Next, you will also have to extend the bar chart controller (Chart.controllers.bar
) and overwrite dataElementType
to use the new "rounded rectangle" for the chart instead of a regular rectangle.
Chart.defaults.roundedBar = Chart.helpers.clone(Chart.defaults.bar);
Chart.controllers.roundedBar = Chart.controllers.bar.extend({
dataElementType: Chart.elements.RoundedTopRectangle
});
Lastly, we will modify the chart's config to use the new chart type created above and add a new options property called barRoundness
to control how round the top is (0 is flat, 1 is a semi-circle).
var ctx = document.getElementById("canvas").getContext("2d");
var myBar = new Chart(ctx, {
type: 'roundedBar',
data: {
labels: ["Car", "Bike", "Walking"],
datasets: [{
label: 'Students',
backgroundColor: chartColors.blue,
data: [
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
]
}, {
label: 'Teachers',
backgroundColor: chartColors.red,
data: [
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
]
}, {
label: 'Visitors',
backgroundColor: chartColors.green,
data: [
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
]
}]
},
options: {
responsive: true,
barRoundness: 1,
title: {
display: true,
text: "Chart.js - Bar Chart with Rounded Tops (drawRoundedTopRectangle Method)"
},
}
});
You can see a full working example at this codepen.
Also, in case you want a slightly different "rounded top" look, here is another codepen that uses a different approach to drawing the top (a single quadratic curve).
The following only customizes the Chart.elements.Rectangle.prototype.draw
- no need to create an entirely new chart type.
Other pros:
- It works well with both vertical & horizontal bar charts
- It works well with both vertical & horizontal stacked bar charts - it only rounds the last box in the series
Noted issue: because this only rounds the box in the last dataset, if the value of the datapoint in the last dataset is < either of the previous data points, the visual top-most box will not be rounded. However, if the last data point is negative and the lowest value, it will round that box on the bottom corners.
Credit: original code belongs to https://github.com/uffo. Code below and linked fiddle demonstrate increasing positive values in each dataset for each stack, and also modifies some of the default radius options.
/**Customize the Rectangle.prototype draw method**/
Chart.elements.Rectangle.prototype.draw = function() {
var ctx = this._chart.ctx;
var vm = this._view;
var left, right, top, bottom, signX, signY, borderSkipped, radius;
var borderWidth = vm.borderWidth;
// If radius is less than 0 or is large enough to cause drawing errors a max
// radius is imposed. If cornerRadius is not defined set it to 0.
var cornerRadius = this._chart.config.options.cornerRadius;
var fullCornerRadius = this._chart.config.options.fullCornerRadius;
var stackedRounded = this._chart.config.options.stackedRounded;
var typeOfChart = this._chart.config.type;
if (cornerRadius < 0) {
cornerRadius = 0;
}
if (typeof cornerRadius == 'undefined') {
cornerRadius = 0;
}
if (typeof fullCornerRadius == 'undefined') {
fullCornerRadius = false;
}
if (typeof stackedRounded == 'undefined') {
stackedRounded = false;
}
if (!vm.horizontal) {
// bar
left = vm.x - vm.width / 2;
right = vm.x + vm.width / 2;
top = vm.y;
bottom = vm.base;
signX = 1;
signY = bottom > top ? 1 : -1;
borderSkipped = vm.borderSkipped || 'bottom';
} else {
// horizontal bar
left = vm.base;
right = vm.x;
top = vm.y - vm.height / 2;
bottom = vm.y + vm.height / 2;
signX = right > left ? 1 : -1;
signY = 1;
borderSkipped = vm.borderSkipped || 'left';
}
// 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 (borderWidth) {
// borderWidth shold be less than bar width and bar height.
var barSize = Math.min(Math.abs(left - right), Math.abs(top - bottom));
borderWidth = borderWidth > barSize ? barSize : borderWidth;
var halfStroke = borderWidth / 2;
// Adjust borderWidth when bar top position is near vm.base(zero).
var borderLeft = left + (borderSkipped !== 'left' ? halfStroke * signX : 0);
var borderRight = right + (borderSkipped !== 'right' ? -halfStroke * signX : 0);
var borderTop = top + (borderSkipped !== 'top' ? halfStroke * signY : 0);
var borderBottom = bottom + (borderSkipped !== 'bottom' ? -halfStroke * signY : 0);
// not become a vertical line?
if (borderLeft !== borderRight) {
top = borderTop;
bottom = borderBottom;
}
// not become a horizontal line?
if (borderTop !== borderBottom) {
left = borderLeft;
right = borderRight;
}
}
ctx.beginPath();
ctx.fillStyle = vm.backgroundColor;
ctx.strokeStyle = vm.borderColor;
ctx.lineWidth = borderWidth;
// Corner points, from bottom-left to bottom-right clockwise
// | 1 2 |
// | 0 3 |
var corners = [
[left, bottom],
[left, top],
[right, top],
[right, bottom]
];
// Find first (starting) corner with fallback to 'bottom'
var borders = ['bottom', 'left', 'top', 'right'];
var startCorner = borders.indexOf(borderSkipped, 0);
if (startCorner === -1) {
startCorner = 0;
}
function cornerAt(index) {
return corners[(startCorner + index) % 4];
}
// Draw rectangle from 'startCorner'
var corner = cornerAt(0);
ctx.moveTo(corner[0], corner[1]);
var nextCornerId, nextCorner, width, height, x, y;
for (var i = 1; i < 4; i++) {
corner = cornerAt(i);
nextCornerId = i + 1;
if (nextCornerId == 4) {
nextCornerId = 0
}
nextCorner = cornerAt(nextCornerId);
width = corners[2][0] - corners[1][0];
height = corners[0][1] - corners[1][1];
x = corners[1][0];
y = corners[1][1];
var radius = cornerRadius;
// Fix radius being too large
if (radius > Math.abs(height) / 2) {
radius = Math.floor(Math.abs(height) / 2);
}
if (radius > Math.abs(width) / 2) {
radius = Math.floor(Math.abs(width) / 2);
}
var x_tl, x_tr, y_tl, y_tr, x_bl, x_br, y_bl, y_br;
if (height < 0) {
// Negative values in a standard bar chart
x_tl = x;
x_tr = x + width;
y_tl = y + height;
y_tr = y + height;
x_bl = x;
x_br = x + width;
y_bl = y;
y_br = y;
// Draw
ctx.moveTo(x_bl + radius, y_bl);
ctx.lineTo(x_br - radius, y_br);
// bottom right
ctx.quadraticCurveTo(x_br, y_br, x_br, y_br - radius);
ctx.lineTo(x_tr, y_tr + radius);
// top right
fullCornerRadius ? ctx.quadraticCurveTo(x_tr, y_tr, x_tr - radius, y_tr) : ctx.lineTo(x_tr, y_tr, x_tr - radius, y_tr);
ctx.lineTo(x_tl + radius, y_tl);
// top left
fullCornerRadius ? ctx.quadraticCurveTo(x_tl, y_tl, x_tl, y_tl + radius) : ctx.lineTo(x_tl, y_tl, x_tl, y_tl + radius);
ctx.lineTo(x_bl, y_bl - radius);
// bottom left
ctx.quadraticCurveTo(x_bl, y_bl, x_bl + radius, y_bl);
} else if (width < 0) {
// Negative values in a horizontal bar chart
x_tl = x + width;
x_tr = x;
y_tl = y;
y_tr = y;
x_bl = x + width;
x_br = x;
y_bl = y + height;
y_br = y + height;
// Draw
ctx.moveTo(x_bl + radius, y_bl);
ctx.lineTo(x_br - radius, y_br);
// Bottom right corner
fullCornerRadius ? ctx.quadraticCurveTo(x_br, y_br, x_br, y_br - radius) : ctx.lineTo(x_br, y_br, x_br, y_br - radius);
ctx.lineTo(x_tr, y_tr + radius);
// top right Corner
fullCornerRadius ? ctx.quadraticCurveTo(x_tr, y_tr, x_tr - radius, y_tr) : ctx.lineTo(x_tr, y_tr, x_tr - radius, y_tr);
ctx.lineTo(x_tl + radius, y_tl);
// top left corner
ctx.quadraticCurveTo(x_tl, y_tl, x_tl, y_tl + radius);
ctx.lineTo(x_bl, y_bl - radius);
// bttom left corner
ctx.quadraticCurveTo(x_bl, y_bl, x_bl + radius, y_bl);
} else {
var lastVisible = 0;
for (var findLast = 0, findLastTo = this._chart.data.datasets.length; findLast < findLastTo; findLast++) {
if (!this._chart.getDatasetMeta(findLast).hidden) {
lastVisible = findLast;
}
}
var rounded = this._datasetIndex === lastVisible;
if (rounded) {
//Positive Value
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
// top right
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
// bottom right
if (fullCornerRadius || typeOfChart == 'horizontalBar')
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
else
ctx.lineTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
// bottom left
if (fullCornerRadius)
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
else
ctx.lineTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
// top left
if (fullCornerRadius || typeOfChart == 'bar')
ctx.quadraticCurveTo(x, y, x + radius, y);
else
ctx.lineTo(x, y, x + radius, y);
}else {
ctx.moveTo(x, y);
ctx.lineTo(x + width, y);
ctx.lineTo(x + width, y + height);
ctx.lineTo(x, y + height);
ctx.lineTo(x, y);
}
}
}
ctx.fill();
if (borderWidth) {
ctx.stroke();
}
};
/**Chart Data**/
var data = {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [{
label: 'data 0',
data: [12, 19, 3, 5, 2, 3],
backgroundColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 0
}, {
label: 'data 1',
data: [20, 24, 10, 15, 12, 13],
backgroundColor: [
'rgba(255, 159, 64, 1)',
'rgba(255, 99, 132, 1)',
'rgba(255, 206, 86, 1)',
'rgba(54, 162, 235, 1)',
'rgba(153, 102, 255, 1)',
'rgba(75, 192, 192, 1)'
],
borderWidth: 0
}, {
label: 'data 2',
data: [20, 30, 30, 20, 14, 20],
backgroundColor: [
'rgba(75, 192, 192, 1)',
'rgba(255, 159, 64, 1)',
'rgba(255, 99, 132, 1)',
'rgba(255, 206, 86, 1)',
'rgba(54, 162, 235, 1)',
'rgba(153, 102, 255, 1)'
],
borderWidth: 0
}]
};
/**Chart Options - Radius options are here**/
var options = {
//Border radius; Default: 0; If a negative value is passed, it will overwrite to 0;
cornerRadius: 10,
//Default: false; if true, this would round all corners of final box;
fullCornerRadius: false,
//Default: false; if true, this rounds each box in the stack instead of only final box;
stackedRounded: false,
elements: {
point: {
radius: 25,
hoverRadius: 35,
pointStyle: 'rectRounded',
}
},
scales: {
yAxes: [{
ticks: {
beginAtZero: true
},
stacked: true,
radius: 25
}],
xAxes: [{
ticks: {
beginAtZero: true
},
stacked: true,
}]
}
};
/**Generate Chart**/
var ctxBar = document.getElementById("myChart");
var myBarChart = new Chart(ctxBar, {
type: 'bar',
data: data,
options: options
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.js"></script>
<canvas id="myChart" height="300" width="800"></canvas>
Fiddle: https://jsfiddle.net/adammoisa/v0dthnyr/7/
Solution by @jordanwillis doesn't work with chart.js version after 2.8.0.
To make it work just add following code before drawRoundedTopRectangle
definition
Chart.helpers.merge(Chart.defaults.global, {
datasets: {
roundedBar: {
categoryPercentage: 0.8,
barPercentage: 0.9
}
}
});
Also worth a mention is this solution which takes you 30 seconds to implement:
https://github.com/jedtrow/Chart.js-Rounded-Bar-Charts
Download and put the .js file into your projects folder, load it and use var options = {
cornerRadius: 20,
};
to get rounded bars.
Credits: https://github.com/jedtrow