I would like to make a doughnut chart using Chart.Js library, with text in the center of the doughnut and the percentage on each segment.
The segments on the chart are generated dynamically.
So I look through Stackoverflow to look for solution, and combined 2 that I found:
How to create a donut chart like this in chart.js
How to add text inside the doughnut chart using Chart.js?
and combine into the following javascript:
Chart.defaults.derivedDoughnut = Chart.defaults.doughnut;
var helpers = Chart.helpers;
var customDN = Chart.controllers.doughnut.extend({
draw: function(ease) {
Chart.controllers.doughnut.prototype.draw.call(this, ease);
if (this.chart.config.options.elements.center) {
//Get ctx from string
var ctx = this.chart.chart.ctx;
//Get options from the center object in options
var centerConfig = this.chart.config.options.elements.center;
var fontStyle = centerConfig.fontStyle || 'Arial';
var txt = centerConfig.text;
var color = centerConfig.color || '#000';
var sidePadding = centerConfig.sidePadding || 20;
var sidePaddingCalculated = (sidePadding/100) * (this.chart.innerRadius * 2)
//Start with a base font of 30px
ctx.font = "30px " + fontStyle;
//Get the width of the string and also the width of the element minus 10 to give it 5px side padding
var stringWidth = ctx.measureText(txt).width;
var elementWidth = (this.chart.innerRadius * 2) - sidePaddingCalculated;
// Find out how much the font can grow in width.
var widthRatio = elementWidth / stringWidth;
var newFontSize = Math.floor(20 * widthRatio);
var elementHeight = (this.chart.innerRadius * 2);
// Pick a new font size so it will not be larger than the height of label.
var fontSizeToUse = Math.min(newFontSize, elementHeight);
//Set font settings to draw it correctly.
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
var centerX = ((this.chart.chartArea.left + this.chart.chartArea.right) / 2);
var centerY = ((this.chart.chartArea.top + this.chart.chartArea.bottom) / 2);
ctx.font = fontSizeToUse+"px " + fontStyle;
ctx.fillStyle = color;
//Draw text in center
ctx.fillText(txt, centerX, centerY);
//labels
var chart = this.chart;
var chartArea = chart.chartArea;
var opts = chart.options;
var meta = this.getMeta();
var arcs = meta.data;
for (i in arcs){
var arcctx = arcs[i]._chart.ctx;
var view = arcs[i]._view;
var sa = view.startAngle;
var ea = view.endAngle;
var opts = arcs[i]._chart.config.options;
var labelPos = arcs[i].tooltipPosition();
var segmentLabel = view.circumference / opts.circumference*100;
arcctx.fillStyle = view.borderColor;
arcctx.font = "1px" + fontStyle;
arcctx.fillText(segmentLabel.toFixed(0) + "%", labelPos.x, labelPos.y);
}
}
}
});
but my outcome is outcome image
I tried fixing the font of the arc, but it still does not work. It seems to utilize the font set for the text in the center of the doughnut.
Can someone tell me what did I do wrong here, how do I set the font size of the arc and text in the center of the doughnut differently? Thanks!
I have a doughnut chart using Chart.js that displays login data for my app correctly, however I have modified the chart so that the total number of logins is displayed in text in the center cutout:
The problem I am running into is with the tooltips. When I hover over the light teal piece of the pie chart, if the chart is scaled smaller, the tooltip is overlapped by the text in the center, like this:
I want to be able to change the direction the tooltip extends out, so instead of it going towards the center, it moves away so that both the tooltip and the center analytic are visible, but I have yet to find a concise explanation on how to change tooltip positioning. Here is the code I have currently:
var loslogged = dataset[0][0].loslogged;
var realtorlogged = dataset[1][0].realtorlogged;
var borrowerlogged = dataset[2][0].borrowerlogged;
var totallogged = parseInt(loslogged) + parseInt(realtorlogged) + parseInt(borrowerlogged);
Chart.pluginService.register({
afterDraw: function (chart) {
if (chart.config.options.elements.center) {
var helpers = Chart.helpers;
var centerX = (chart.chartArea.left + chart.chartArea.right) / 2;
var centerY = (chart.chartArea.top + chart.chartArea.bottom) / 2;
var ctx = chart.chart.ctx;
ctx.save();
var fontSize = helpers.getValueOrDefault(chart.config.options.elements.center.fontSize, Chart.defaults.global.defaultFontSize);
var fontStyle = helpers.getValueOrDefault(chart.config.options.elements.center.fontStyle, Chart.defaults.global.defaultFontStyle);
var fontFamily = helpers.getValueOrDefault(chart.config.options.elements.center.fontFamily, Chart.defaults.global.defaultFontFamily);
var font = helpers.fontString(fontSize, fontStyle, fontFamily);
ctx.font = font;
ctx.fillStyle = helpers.getValueOrDefault(chart.config.options.elements.center.fontColor, Chart.defaults.global.defaultFontColor);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(chart.config.options.elements.center.text, centerX, centerY);
ctx.restore();
}
}
});
var loginChartData = {
labels: ["Loan Officers","Realtors","Borrowers"],
datasets: [{
label: "Number of Logins",
data: [loslogged, realtorlogged, borrowerlogged],
backgroundColor: [
"rgba(191, 25, 25, 0.75)",
"rgba(58, 73, 208, 0.75)",
"rgba(79, 201, 188, 0.75)"
],
borderColor: [
"rgba(255, 255, 255, 1)",
"rgba(255, 255, 255, 1)",
"rgba(255, 255, 255, 1)"
],
borderWidth: 4
}],
gridLines: {
display: false
}
};
var loginChartOptions = {
title: {
display: false
},
cutoutPercentage: 50,
elements: {
center: {
text: totallogged,
fontColor: '#000',
fontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
fontSize: 36,
fontStyle: 'bold'
}
}
};
var loginChart = document.getElementById('loginsChart').getContext('2d');
new Chart(loginChart, {
type: 'doughnut',
data: loginChartData,
options: loginChartOptions
});
It used to be a lot easier to reverse the tooltips in previous versions of chart.js (v2.3 and before). All you had to do was overwrite the determineAlignment tooltip method and reverse the logic.
However starting in v2.4, the functions that calculate the tooltip positions (including determineAlignment) were made private, so there is no longer a way to simply overwrite them (instead you have to duplicate them).
Here is a working reversed tooltip solution that unfortunately requires a lot of copy and paste from the chart.js source (this is required since the methods are private). The risk with this approach is that the underlying private functions could change in new releases at any time and your new reverse tooltip could break unexpectedly.
With that said, here is walk through of the implementation (with a codepen example at the bottom).
1) First, let's extend the Chart.Tooltip object and create a new Chart.ReversedTooltip object. We really only need to overwrite the update method since it performs all the positioning logic. In fact, this overwrite is just a straight copy and paste from the source because we actually only need to modify the private determineAlignment method which is called by update.
// create a new reversed tooltip. we must overwrite the update method which is
// where all the positioning occurs
Chart.ReversedTooltip = Chart.Tooltip.extend({
update: function(changed) {
var me = this;
var opts = me._options;
// Need to regenerate the model because its faster than using extend and it is necessary due to the optimization in Chart.Element.transition
// that does _view = _model if ease === 1. This causes the 2nd tooltip update to set properties in both the view and model at the same time
// which breaks any animations.
var existingModel = me._model;
var model = me._model = getBaseModel(opts);
var active = me._active;
var data = me._data;
var chartInstance = me._chartInstance;
// In the case where active.length === 0 we need to keep these at existing values for good animations
var alignment = {
xAlign: existingModel.xAlign,
yAlign: existingModel.yAlign
};
var backgroundPoint = {
x: existingModel.x,
y: existingModel.y
};
var tooltipSize = {
width: existingModel.width,
height: existingModel.height
};
var tooltipPosition = {
x: existingModel.caretX,
y: existingModel.caretY
};
var i, len;
if (active.length) {
model.opacity = 1;
var labelColors = [];
tooltipPosition = Chart.Tooltip.positioners[opts.position](active, me._eventPosition);
var tooltipItems = [];
for (i = 0, len = active.length; i < len; ++i) {
tooltipItems.push(createTooltipItem(active[i]));
}
// If the user provided a filter function, use it to modify the tooltip items
if (opts.filter) {
tooltipItems = tooltipItems.filter(function(a) {
return opts.filter(a, data);
});
}
// If the user provided a sorting function, use it to modify the tooltip items
if (opts.itemSort) {
tooltipItems = tooltipItems.sort(function(a, b) {
return opts.itemSort(a, b, data);
});
}
// Determine colors for boxes
helpers.each(tooltipItems, function(tooltipItem) {
labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, chartInstance));
});
// Build the Text Lines
model.title = me.getTitle(tooltipItems, data);
model.beforeBody = me.getBeforeBody(tooltipItems, data);
model.body = me.getBody(tooltipItems, data);
model.afterBody = me.getAfterBody(tooltipItems, data);
model.footer = me.getFooter(tooltipItems, data);
// Initial positioning and colors
model.x = Math.round(tooltipPosition.x);
model.y = Math.round(tooltipPosition.y);
model.caretPadding = helpers.getValueOrDefault(tooltipPosition.padding, 2);
model.labelColors = labelColors;
// data points
model.dataPoints = tooltipItems;
// We need to determine alignment of the tooltip
tooltipSize = getTooltipSize(this, model);
alignment = determineAlignment(this, tooltipSize);
// Final Size and Position
backgroundPoint = getBackgroundPoint(model, tooltipSize, alignment);
} else {
model.opacity = 0;
}
model.xAlign = alignment.xAlign;
model.yAlign = alignment.yAlign;
model.x = backgroundPoint.x;
model.y = backgroundPoint.y;
model.width = tooltipSize.width;
model.height = tooltipSize.height;
// Point where the caret on the tooltip points to
model.caretX = tooltipPosition.x;
model.caretY = tooltipPosition.y;
me._model = model;
if (changed && opts.custom) {
opts.custom.call(me, model);
}
return me;
},
});
2) As you can see, the update method uses a handful of private methods (e.g. getBaseModel, createTooltipItem, determineAlignment, etc.). In order for our update method to actually work, we have to provide an implementation for each of these methods. Here again is another copy and paste from the source. The only method that we need to modify however is the determineAlignment method. Here is the modified version that reverses the alignment logic.
// modified from source to reverse the position
function determineAlignment(tooltip, size) {
var model = tooltip._model;
var chart = tooltip._chart;
var chartArea = tooltip._chartInstance.chartArea;
var xAlign = 'center';
var yAlign = 'center';
// set caret position to top or bottom if tooltip y position will extend outsite the chart top/bottom
if (model.y < size.height) {
yAlign = 'top';
} else if (model.y > (chart.height - size.height)) {
yAlign = 'bottom';
}
var leftAlign, rightAlign; // functions to determine left, right alignment
var overflowLeft, overflowRight; // functions to determine if left/right alignment causes tooltip to go outside chart
var yAlign; // function to get the y alignment if the tooltip goes outside of the left or right edges
var midX = (chartArea.left + chartArea.right) / 2;
var midY = (chartArea.top + chartArea.bottom) / 2;
if (yAlign === 'center') {
leftAlign = function(x) {
return x >= midX;
};
rightAlign = function(x) {
return x < midX;
};
} else {
leftAlign = function(x) {
return x <= (size.width / 2);
};
rightAlign = function(x) {
return x >= (chart.width - (size.width / 2));
};
}
overflowLeft = function(x) {
return x - size.width < 0;
};
overflowRight = function(x) {
return x + size.width > chart.width;
};
yAlign = function(y) {
return y <= midY ? 'bottom' : 'top';
};
if (leftAlign(model.x)) {
xAlign = 'left';
// Is tooltip too wide and goes over the right side of the chart.?
if (overflowLeft(model.x)) {
xAlign = 'center';
yAlign = yAlign(model.y);
}
} else if (rightAlign(model.x)) {
xAlign = 'right';
// Is tooltip too wide and goes outside left edge of canvas?
if (overflowRight(model.x)) {
xAlign = 'center';
yAlign = yAlign(model.y);
}
}
var opts = tooltip._options;
return {
xAlign: opts.xAlign ? opts.xAlign : xAlign,
yAlign: opts.yAlign ? opts.yAlign : yAlign
};
};
3) Now that our new Chart.ReversedTooltip is complete, we need to use the plugin system to change the original tooltip to our reversed tooltip. We can do this using the afterInit plugin method.
Chart.plugins.register({
afterInit: function (chartInstance) {
// replace the original tooltip with the reversed tooltip
chartInstance.tooltip = new Chart.ReversedTooltip({
_chart: chartInstance.chart,
_chartInstance: chartInstance,
_data: chartInstance.data,
_options: chartInstance.options.tooltips
}, chartInstance);
chartInstance.tooltip.initialize();
}
});
After all that, we finally have reversed tooltips! Checkout a full working example at this codepen.
It's also worth mentioning that this approach is very brittle and, as I mentioned, can easily break overtime (on account of the copy and pasting required). Another option would be to just use a custom tooltip instead and position it wherever you desire on the chart.
Checkout this chart.js sample that shows how to setup and use a custom tooltip. You could go with this approach and just modify the positioning logic.
If you have a small tooltip label, you can use simple chart.js options to fix overlaps issue:
plugins: {
tooltip: {
xAlign: 'center',
yAlign: 'bottom'
}
}
I managed to solve the same by setting zIndex of Doughnut wrapper div to 1, settting the zIndex of text shown in the middle of Doughnut to -1, and canvas is transparent by default.
Hope this hels.
I'm using jquery.flot.barnumbers.js plugin for the Javascript plotting (charts) library for jQuery to show the numbers on the bars.
My code:
$.plot("#placeholderByDay", [
{
data: DataOne, label: "Total Calls", bars: {
numbers:{
show:true,
xAlign: 80,//align top
yAlign: 1
//yAlign: function(y) { return y+ 1; } //upside of bars
}
} ]);
What I get now is:
What I need is:
So when there is no data for a bar, the zeros should be just above the axis, and where there are values should be as is, both rotated let 90 degrees. How can I achieve this?
Oops, I did it again.
If you'd like to drop the plugin and do this the fun way; code it up yourself. It'll give you the freedom to customize any way you like.
// after you draw the plot
var ctx = somePlot.getCanvas().getContext("2d");
var data = somePlot.getData()[0].data;
var xaxis = somePlot.getXAxes()[0];
var yaxis = somePlot.getYAxes()[0];
var offset = somePlot.getPlotOffset();
ctx.font = "16px 'Segoe UI'";
ctx.fillStyle = "black";
for (var i = 0; i < data.length; i++){
var text = data[i][4] + '';
var metrics = ctx.measureText(text);
var xPos = xaxis.p2c(data[i][0]) + offset.left;
var yPos = yaxis.p2c(data[i][5]) + offset.top + metrics.width + 5;
// perform the rotation
ctx.save();
ctx.translate(xPos, yPos);
ctx.rotate(-Math.PI/2);
ctx.fillText(text, 1, 1);
ctx.restore();
}
Example here.
part of the plot
I use "categories" in yaxis,the y position of the text should be like that:
for(var i=0;i<data1.length;i++){
.....
var yPos = yaxis.p2c(i) + fontSize;
.....
}
if you want the flot shows like that:
the y position of the text should be like that:
for(var i=0;i<data1.length;i++){
.....
var yPos = yaxis.p2c(i+align) + fontSize;
.....
}
align = barWidth/2, if bars' align = 'left';
align = -barWidth/2, if bars' align = 'right;
I'm using jquery.flot.barnumbers.js plugin for the Javascript plotting (charts) library for jQuery to show the numbers on the bars.
My code:
$.plot("#placeholderByDay", [
{
data: DataOne, label: "Total Calls", bars: {
numbers:{
show:true,
xAlign: 80,//align top
yAlign: 1
//yAlign: function(y) { return y+ 1; } //upside of bars
}
} ]);
What I get now is:
What I need is:
So when there is no data for a bar, the zeros should be just above the axis, and where there are values should be as is, both rotated let 90 degrees. How can I achieve this?
Oops, I did it again.
If you'd like to drop the plugin and do this the fun way; code it up yourself. It'll give you the freedom to customize any way you like.
// after you draw the plot
var ctx = somePlot.getCanvas().getContext("2d");
var data = somePlot.getData()[0].data;
var xaxis = somePlot.getXAxes()[0];
var yaxis = somePlot.getYAxes()[0];
var offset = somePlot.getPlotOffset();
ctx.font = "16px 'Segoe UI'";
ctx.fillStyle = "black";
for (var i = 0; i < data.length; i++){
var text = data[i][4] + '';
var metrics = ctx.measureText(text);
var xPos = xaxis.p2c(data[i][0]) + offset.left;
var yPos = yaxis.p2c(data[i][5]) + offset.top + metrics.width + 5;
// perform the rotation
ctx.save();
ctx.translate(xPos, yPos);
ctx.rotate(-Math.PI/2);
ctx.fillText(text, 1, 1);
ctx.restore();
}
Example here.
part of the plot
I use "categories" in yaxis,the y position of the text should be like that:
for(var i=0;i<data1.length;i++){
.....
var yPos = yaxis.p2c(i) + fontSize;
.....
}
if you want the flot shows like that:
the y position of the text should be like that:
for(var i=0;i<data1.length;i++){
.....
var yPos = yaxis.p2c(i+align) + fontSize;
.....
}
align = barWidth/2, if bars' align = 'left';
align = -barWidth/2, if bars' align = 'right;
I'm using chart.Js to display my chart. I'm getting my chart data via ajax and graphically render it to display the data,
My question is, there are few cases when my Ajax returns nothing, and my Chart just display X axis and Y axis, with no data or no legend shown. Is there any option to show default text ?
PS: I know I can add some conditional statement and display "No chart Div" and hide my "chart div", but i was some clean method to do the same.
You can try this solution:
if(1 < chartData.labels.length) {
chart = new Chart(options.ctx).Line(chartData, options.chartOptions);
} else {
options.ctx.font = "20px " + Chart.defaults.global.tooltipTitleFontFamily;
options.ctx.textAlign = "center";
options.ctx.textBaseline = "middle";
options.ctx.fillStyle = Chart.defaults.global.scaleFontColor;
options.ctx.fillText("No data in chart.", options.ctx.canvas.clientWidth / 2, options.ctx.canvas.clientHeight / 2);
}
This works for me I hope it helps
Chart.pluginService.register({
afterDraw: function (chart) {
if (chart.data.datasets[0].data.length === 0) {
// No data is present
var ctx = chart.chart.ctx;
var width = chart.chart.width;
var height = chart.chart.height
chart.clear();
ctx.save();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = "20px normal 'Helvetica Nueue'";
ctx.fillText('No data to display', width / 2, height / 2);
ctx.restore();
}
}
});