Related
I have a canvas (chart) that i would like to add text upon ( sum of calculations, or more data by text) is there a way to make the text apear in front of the canvas and not be hidden by it?
(i tried to add text over the canvas, and it was hidden by it.. )
Yes, it's fairly simple to add text to the canvas. You just need a variable to reference the data that you want to display and then use the canvas api build the text as I've shown below.
elements added to the canvas are, by default, placed above the previously added elements, so as long as your text is created after your chart, it should appear above it.
// make the canvas element and add it to the DOM
let canvas = document.createElement("canvas");
canvas.width = 256;
canvas.height = 256;
canvas.style.border = "1px solid black";
canvas.style.backgroundColor = "white";
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
//Create a text string defines the content you want to display
//This could be updated dynamically in your program
let content = "My Data";
//Assign the font to the canvas context.
//The first value is the font size, followed by the names of the
//font families that should be tried:
//1st choice, fall-back font, and system font fall-back
ctx.font = "14px 'Rockwell Extra Bold', 'Futura', sans-serif";
//Set the font color to red
ctx.fillStyle = "red";
//Figure out the width and height of the text
let width = ctx.measureText(content).width,
height = ctx.measureText("M").width;
//Set the text's x/y registration point to its top left corner
ctx.textBaseline = "top";
//Use `fillText` to Draw the text in the center of the canvas
ctx.fillText(
content, //The text string
10, //The x position
canvas.height / 2 - height / 2 //The y position
);
Or, to ensure that the chart image is loaded before we draw anything else on top of it we can do something like this:
// make the canvas element and add it to the DOM
let canvas = document.createElement("canvas");
canvas.width = 360;
canvas.height = 180;
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
// set the colour of the canvas background
canvas.style.backgroundColor = "#999";
canvas.width = 360; // 360deg
canvas.height = 180;
// declare var to hold our chart/background
let worldImg;
// load our inage or run the code to
// draw the chart
let loadImg = new Promise(() => {
//Load an image
worldImg = new Image();
worldImg.addEventListener("load", loadHandler, false);
worldImg.src = "https://stuartcodes.000webhostapp.com/pexels-pixabay-41949.png";
console.log(worldImg);
});
//The loadHandler is called when the image has loaded
function loadHandler() {
console.log("OK");
// draw the image that we want as the background
ctx.drawImage(worldImg, 0, 0);
// then draw everything else
/****** Your Data ******\ */
//Create a text string defines the content you want to display
//This could be updated dynamically in your program
let content = "My Data";
//Assign the font to the canvas context.
//The first value is the font size, followed by the names of the
//font families that should be tried:
//1st choice, fall-back font, and system font fall-back
ctx.font = "14px 'Rockwell Extra Bold', 'Futura', sans-serif";
//Set the font color to red
ctx.fillStyle = "orange";
//Figure out the width and height of the text
let width = ctx.measureText(content).width,
height = ctx.measureText("M").width;
//Set the text's x/y registration point to its top left corner
ctx.textBaseline = "top";
//Use `fillText` to Draw the text in the center of the canvas
ctx.fillText(
content, //The text string
10, //The x position
canvas.height / 2 - height / 2 //The y position
);
}
in the Doughnut chart definition, i added the following:
Chart.pluginService.register({
beforeDraw: function(chart) {
if(chart.chart.config.type=="doughnut" && chart.canvas.id=="position01"){
chart.clear();
var width = chart.chart.width,
height = chart.chart.height,
ctx = chart.chart.ctx;
// ctx.restore();
var fontSize = (height / 200).toFixed(2);
ctx.font = fontSize + "em sans-serif";
ctx.textBaseline = "middle";
var sum = 0;
input.data.map(p=>sum+=p);
var ductCostText = "Ducts Cost:" ;//was like that: textX = Math.round((width - ctx.measureText(text).width) / 2),
var costText = numberWithCommas(sum.toFixed(2)*200) + "₪";
textX = 0;// Xcoordinate for the text
textY = height / 2-120;// the Y coordinant of the cost header
ctx.fillText(ductCostText, textX, textY);
}
}
});
// formatting function : format as a price
function numberWithCommas(x) {
return x.toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
In every tutorial that I could find for how to rotate a sprite image on a canvas the canvas itself is rotated before applying sprite to it:
function drawSprite(sprite, x, y, deg)
{
const width = sprite.width / 2,
height = sprite.height / 2;
x = x + width;
y = y + height;
//clear main canvas
mainCtx.fillRect(0, 0, mainCanvas.width, mainCanvas.height);
// move origin to the coordinates of the center where sprite will be drawn
mainCtx.translate(x, y);
// rotate canvas
mainCtx.rotate(deg);
// draw sprite
mainCtx.drawImage(sprite, -width, -height);
// restore previous rotation and origin
mainCtx.rotate(-deg);
mainCtx.translate(-x, -y);
}
//never mind the rest
const mainCtx = mainCanvas.getContext("2d"),
sprite = (() =>
{
const canvas = document.createElement("canvas"),
ctx = canvas.getContext("2d"),
width = canvas.width = ctx.width = 100,
height = canvas.height = ctx.height = 50;
ctx.font = '20px arial';
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = "lightgreen";
ctx.fillRect(0, 0, width, height);
ctx.strokeRect(0, 0, width, height);
ctx.strokeText("my sprite", width/2, height/2);
return canvas;
})();
let r = 0;
const d = Math.sqrt(sprite.width *sprite.width + sprite.height*sprite.height),
w = mainCanvas.width = mainCtx.width = 400,
h = mainCanvas.height = mainCtx.height = 200;
mainCtx.fillStyle = "pink";
setInterval(() =>
{
const deg = r++*Math.PI/180;
let x = ((w-d)/2) + (Math.sin(deg)*((w-d)/2)),
y = ((h-d)/1.2) + (Math.cos(deg)*((h-d)/2));
drawSprite(sprite, x, y, deg);
}, 10);
<canvas id="mainCanvas"></canvas>
To me this is counterintuitive, why can't we rotate sprite itself before drawing it on main canvas? Why doesn't this work?
function drawSprite(sprite, x, y, deg)
{
const spriteCtx = sprite.getContext("2d");
//clear main canvas
mainCtx.fillRect(0, 0, mainCanvas.width, mainCanvas.height);
// rotate sprite
spriteCtx.rotate(deg);
// draw sprite
mainCtx.drawImage(sprite, x, y);
}
//never mind the rest
const mainCtx = mainCanvas.getContext("2d"),
sprite = (() =>
{
const canvas = document.createElement("canvas"),
ctx = canvas.getContext("2d"),
width = canvas.width = ctx.width = 100,
height = canvas.height = ctx.height = 50;
ctx.font = '20px arial';
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = "lightgreen";
ctx.fillRect(0, 0, width, height);
ctx.strokeRect(0, 0, width, height);
ctx.strokeText("my sprite", width/2, height/2);
return canvas;
})();
let r = 0;
const d = Math.sqrt(sprite.width *sprite.width + sprite.height*sprite.height),
w = mainCanvas.width = mainCtx.width = 400,
h = mainCanvas.height = mainCtx.height = 200;
mainCtx.fillStyle = "pink";
setInterval(() =>
{
const deg = r++*Math.PI/180;
let x = ((w-d)/2) + (Math.sin(deg)*((w-d)/2)),
y = ((h-d)/1.2) + (Math.cos(deg)*((h-d)/2));
drawSprite(sprite, x, y, deg);
}, 10);
<canvas id="mainCanvas"></canvas>
Wouldn't it be faster rotate a 100x100 sprite vs 10000x10000 main canvas?
Because the drawImage function takes only x(s),y(s) coordinate and width(s),/height(s).
I.e it only ever draws a straight rectangle, there is no way to make it draw anything skewed.
So you have to rotate the context's Current Transformation Matrix (CTM), which is not the canvas, so that the drawing is transformed.
Note that drawing a bitmap as a rectangle is a very basic model for drawing APIs.
As for the speed, once again you don't rotate the canvas, only the CTM and this only affects the future drawings and costs almost nothing anyway.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.font = "30px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.translate(150, 75);
const txtArr = [ "first", "second", "third", "fourth" ];
const colors = [ "red", "blue", "green", "orange" ];
for (let i = 0; i < txtArr.length; i++) {
ctx.fillStyle = colors[i];
ctx.fillText(txtArr[i], 0, 0);
// This doesn't rotate the previous drawings
ctx.rotate(Math.PI / txtArr.length);
}
<canvas></canvas>
So, yes, you could have your own drawImage(source, matrix) which would be something like
this.save();
this.setTransform(matrix);
this.drawImage(source, 0, 0);
this.restore();
But as you can see this means actually more operations per draw call, and thus performing two drawings on the same CTM would actually cost more than setting the CTM only once.
I have this Chart JS Bar chart
Here the custom option used to render labels on top, but if the bar is exact size of max Y-axis value the label gets hidden or goes out of chart.
Is there any way to get the label outside of chart or increase Y-axis value?
Code:
var barChart = new Chart(document.getElementById("third_party_chart"), {
type: 'bar',
data: response,
options: {
responsive: true,
legend: {
display: false,
},
"animation": {
"duration": 1,
"onComplete": function() {
var chartInstance = this.chart,
ctx = chartInstance.ctx;
ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontSize, Chart.defaults.global.defaultFontStyle, Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
this.data.datasets.forEach(function(dataset, i) {
var meta = chartInstance.controller.getDatasetMeta(i);
meta.data.forEach(function(bar, index) {
var data = dataset.data[index];
ctx.fillText(data, bar._model.x, bar._model.y - 5);
});
});
}
}
}
});
I created a Donut chart, which works correctly but now I need to show the number 45 in the center of this, for example.
Where should I indicate the text to be displayed and the coordinates? In the options of the chart?
I'm using react component
class DoughnutChart extends React.Component {
render() {
const data = {
datasets: [{
data: [],
backgroundColor: [
'#999',
'#eee'
]
}],
text: '45'
};
return (
<Doughnut data={data} />
);
}
};
export default DoughnutChart;
EDIT
I found this plugin, but I cann't find how it applies to the react component
//Plugin for center text
Chart.pluginService.register({
beforeDraw: function(chart) {
var width = chart.chart.width,
height = chart.chart.height,
ctx = chart.chart.ctx;
ctx.restore();
var fontSize = (height / 160).toFixed(2);
ctx.font = fontSize + "em sans-serif";
ctx.textBaseline = "top";
var text = "Foo-bar",
textX = Math.round((width - ctx.measureText(text).width) / 2),
textY = height / 2;
ctx.fillText(text, textX, textY);
ctx.save();
}
});
Thanks for your help.
I tried this and it worked
import { Doughnut } from "react-chartjs-2";
function DoughnutChart() {
const data = {...}
const options = {...}
const plugins = [{
beforeDraw: function(chart) {
var width = chart.width,
height = chart.height,
ctx = chart.ctx;
ctx.restore();
var fontSize = (height / 160).toFixed(2);
ctx.font = fontSize + "em sans-serif";
ctx.textBaseline = "top";
var text = "Foo-bar",
textX = Math.round((width - ctx.measureText(text).width) / 2),
textY = height / 2;
ctx.fillText(text, textX, textY);
ctx.save();
}
}]
return (
<Doughnut
type="doughnut"
data={data}
options{options}
plugins={plugins}
/>
);
}
export default DoughnutChart;
I don't believe React Chart JS has this option available. However, there are custom plugins out there that allow you to insert custom data labels inside of your doughnut charts. Please refer to this documentation on how to accomplish that.
ChartJS Plugin Datalabels
From what I've seen from react-chartjs-2 it only has these properties:
data: (PropTypes.object | PropTypes.func).isRequired,
width: PropTypes.number,
height: PropTypes.number,
id: PropTypes.string,
legend: PropTypes.object,
options: PropTypes.object,
redraw: PropTypes.bool,
getDatasetAtEvent: PropTypes.func,
getElementAtEvent: PropTypes.func,
getElementsAtEvent: PropTypes.func
onElementsClick: PropTypes.func, // alias for getElementsAtEvent (backward compatibility)
Non of them seem to be text you can pass in to show your number in the middle. You can maybe use the legend prop and manipulate the object created for the legend to move it with css or have another div inside your render that displays the number and then style that to be inside the chart.
Another way you may be able to do it is by wrapping the Doughnut component in a div with another child inside that div that contains the text, like so:
return(
<div className='wrapper'>
<div className='chart-number'>{this.state.text}</div>
<Doughnut data={data} />
</div>)
in your css then you will have to figure out how to position the chart-number div in the middle of the wrapper div, which will contain the chart.
If you are just starting to use this library, maybe look for a different one that suits your needs so you don't have to code around it!
Hope this helps :)
EDIT 1:
From what I see, you probably need to connect the Chart.pluginService.register code to your graph object somehow which is the <Doughnut /> component, then it will do the calculation before the redraw.
Here is a simple solution:
Step 01: add this options in your options
options: {
centerText: {
display: true,
text: `90%`
}
.....
.....
Step 02: Create a new method drawInnerText
drawInnerText = (chart) => {
var width = chart.chart.width,
height = chart.chart.height,
ctx = chart.chart.ctx;
ctx.restore();
var fontSize = (height / 114).toFixed(2);
ctx.font = fontSize + "em sans-serif";
ctx.textBaseline = "middle";
var text = chart.config.options.centerText.text,
textX = Math.round((width - ctx.measureText(text).width) / 2),
textY = height / 2;
ctx.fillText(text, textX, textY);
ctx.save();
}
Step 03: Invoke drawInnerText
componentWillMount() {
Chart.Chart.pluginService.register({
beforeDraw: function (chart) {
if (chart.config.options.centerText.display !== null &&
typeof chart.config.options.centerText.display !== 'undefined' &&
chart.config.options.centerText.display) {
this.drawInnerText(chart);
}
},
});
}
function DoughnutChart({ data = {} }) {
return (
<Doughnut
data={format(dataObj)}
plugins={[
{
beforeDraw(chart) {
const { width } = chart;
const { height } = chart;
const { ctx } = chart;
ctx.restore();
const fontSize = (height / 160).toFixed(2);
ctx.font = `${fontSize}em sans-serif`;
ctx.textBaseline = 'top';
const { text } = "23";
const textX = Math.round((width - ctx.measureText(text).width) / 2);
const textY = height / 2;
ctx.fillText(text, textX, textY);
ctx.save();
},
},
]}
/>);
}
I tried Jays Answer before but nowadays it doesnt work anymore.
You need to specify an id inside the plugins array.
const plugins = [{
id : 'here comes your id for the specific plugin',
beforeDraw: function(chart) {
var width = chart.width,
height = chart.height,
ctx = chart.ctx;
ctx.restore();
var fontSize = (height / 160).toFixed(2);
ctx.font = fontSize + "em sans-serif";
ctx.textBaseline = "top";
var text = "Foo-bar",
textX = Math.round((width - ctx.measureText(text).width) / 2),
textY = height / 2;
ctx.fillText(text, textX, textY);
ctx.save();
}
}]
I have tried Jay answer and at first it did not work so I had to make some updates to it and now it is working.
const plugins = {[
{
id:"Id here",
beforeDraw:function(chart:any) {
let ctx = chart.ctx;
let xAxis = chart.scales.x;
let yAxis = chart.scales.y;
let maxValue = Math.max(...chart.data.datasets[0].data);
let minValue = Math.min(...chart.data.datasets[0].data);
ctx.save();
ctx.textAlign = 'center';
ctx.font = '14px Roboto';
ctx.fillStyle = 'blue';
ctx.textAlign = 'left';
ctx.fillText('Text1 = ', xAxis.left , yAxis.top + 15);
ctx.fillText('Text2 = ', xAxis.left , yAxis.top + 40);
ctx.fillText(maxValue + '°C', xAxis.left + 200, yAxis.top + 15);
ctx.fillText(minValue + '°C', xAxis.left + 200, yAxis.top + 40);
ctx.restore();
},
},
]}
I have a web page that is using Chart.js. In this page, I am rendering three donut charts. In the middle of each chart, I want to show the percentage of the donut that is filled. Currently, I have the following code, which can be seen in this Fiddle.
function setupChart(chartId, progress) {
var canvas = document.getElementById(chartId);
var context = canvas.getContext('2d');
var remaining = 100 - progress;
var data = {
labels: [ 'Progress', '', ],
datasets: [{
data: [progress, remaining],
backgroundColor: [
'#8FF400',
'#FFFFFF'
],
borderColor: [
'#8FF400',
'#408A00'
],
hoverBackgroundColor: [
'#8FF400',
'#FFFFFF'
]
}]
};
var options = {
responsive: true,
maintainAspectRatio: false,
scaleShowVerticalLines: false,
cutoutPercentage: 80,
legend: {
display: false
},
animation: {
onComplete: function () {
var xCenter = (canvas.width / 2) - 20;
var yCenter = canvas.height / 2 - 40;
context.textAlign = 'center';
context.textBaseline = 'middle';
var progressLabel = data.datasets[0].data[0] + '%';
context.font = '20px Helvetica';
context.fillStyle = 'black';
context.fillText(progressLabel, xCenter, yCenter);
}
}
};
Chart.defaults.global.tooltips.enabled = false;
var chart = new Chart(context, {
type: 'doughnut',
data: data,
options: options
});
}
The chart "runs". But the problem is with the rendering of the label. The label is not vertically and horizontally centered within the middle of the donut. The position of the label changes based on the screen resolution too. You can see the label change position by resizing the frame that the charts are rendered in within the Fiddle.
My question is, how do I consistently position the percentage in the middle of the donut? My page is a responsive page so, having consistent positioning is important Yet, I'm not sure what else to do. I feel like the textAlign and textBaseline properties aren't working, or I'm misunderstanding their usage.
Thanks!
According to this answer, you could do it with a plugin in a further version. I don't know if you noticed, but if you increase the width and reduce it over and over again on your fiddle, the height of your canvas gets bigger and bigger (the chart goes further and further down).
On the version you are using, you could extend the doughnut controller like this:
http://jsfiddle.net/ivanchaer/cutkqkuz/1/
Chart.defaults.DoughnutTextInside = Chart.helpers.clone(Chart.defaults.doughnut);
Chart.controllers.DoughnutTextInside = Chart.controllers.doughnut.extend({
draw: function(ease) {
var ctx = this.chart.chart.ctx;
var data = this.getDataset();
var easingDecimal = ease || 1;
Chart.helpers.each(this.getDataset().metaData, function(arc, index) {
arc.transition(easingDecimal).draw();
var vm = arc._view;
var radius = (vm.outerRadius + vm.innerRadius) / 2;
var thickness = (vm.outerRadius - vm.innerRadius) / 2;
var angle = Math.PI - vm.endAngle - Math.PI / 2;
ctx.save();
ctx.fillStyle = vm.backgroundColor;
ctx.font = "20px Verdana";
ctx.textAlign = 'center';
ctx.textBaseline = "middle";
var text = data.data[0] + '%';
ctx.fillStyle = '#000';
ctx.fillText(text, $(ctx.canvas).width()/2, $(ctx.canvas).height()/2);
});
ctx.fillStyle = '#fff';
ctx.fill();
ctx.restore();
}
});
function setupChart(chartId, progress) {
var canvas = document.getElementById(chartId);
var context = canvas.getContext('2d');
var remaining = 100 - progress;
var deliveredData = {
labels: [
"Value"
],
datasets: [{
data: [progress, remaining],
backgroundColor: [
'#8FF400',
'#FFFFFF'
],
borderColor: [
'#8FF400',
'#408A00'
],
hoverBackgroundColor: [
'#8FF400',
'#FFFFFF'
]
}]
};
var deliveredOpt = {
cutoutPercentage: 88,
animation: false,
legend: {
display: false
},
tooltips: {
enabled: false
}
};
var chart = new Chart(canvas, {
type: 'DoughnutTextInside',
data: deliveredData,
options: deliveredOpt
});
}
setupChart('standChart', 33);
setupChart('moveChart', 66);
setupChart('exerciseChart', 99);
See this http://jsfiddle.net/uha5tseb/2/
It can be handled with getting width/height of each chart by this reference
onComplete: function (event) {
console.log(this.chart.height);
var xCenter = this.chart.width/2;
var yCenter = this.chart.height/2;
context.textAlign = 'center';
context.textBaseline = 'middle';
var progressLabel = data.datasets[0].data[0] + '%';
context.font = '20px Helvetica';
context.fillStyle = 'black';
context.fillText(progressLabel, xCenter, yCenter);
}
Hope this solves your problem!
You set your variables incorrectly when you get xCenter and yCenter. Currently they don't get the middle of the canvas.
You have:
var xCenter = (canvas.width / 2) - 20;
var yCenter = canvas.height / 2 - 40;
It should be:
var xCenter = (canvas.width / 2);
var yCenter = (canvas.height / 2);
Im not familiar with chart.js, but as far as I understood from the documentation, they provide generateLegend method for your purposes. It returns a data from legendCallback that you can display on the page outside of canvas.
Container of the legend text can be aligned relatively to the canvas's wrapper.
HTML:
<div class="chart" data-legend="">
<canvas id="standChart"></canvas>
</div>
JS:
var container = canvas.parentElement;
...
container.setAttribute('data-legend', chart.generateLegend());
Fiddle
One way to resolve this is by adding an absolutely positioned element on top of the canvas.
You can add a div element after each canvas element and then set its style to:
.precentage {
font-family: tahoma;
font-size: 28px;
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
Here is a complete JSFiddle.