Add text over canvas chart javascript - javascript

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, ",");
}

Related

Javascript draw white box with a number in the center - number always a little off

I am drawing a white box via javascript and would like to also draw a number in the center of the box but it doesn't matter how I try to change the positioning, the number is always a little off:
// Create a canvas element
const canvas = document.getElementsByTagName("canvas")[0]
canvas.width = 150;
canvas.height = 150;
// Get the drawing context
const ctx = canvas.getContext('2d');
drawNumberBox(ctx, 123, 0,0, 100, 100, "arial");
function drawNumberBox(ctx, number, x, y, width, height, fontName) {
// Draw the white background rectangle
ctx.fillStyle = 'white';
ctx.fillRect(x, y, width, height);
// Set the font and text alignment
ctx.font = `100px ${fontName}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
// Check if the number fits within the box
while (ctx.measureText(number).width > width) {
// Decrease the font size until the number fits within the box
ctx.font = `${parseInt(ctx.font) - 1}px ${fontName}`;
}
// Draw the number centered inside the box
const xPos = x + (width / 2);
const yPos = y + (height / 2)
ctx.fillText(number, xPos, yPos);
}
<html><body>
<canvas style="background:blue; border:red solid 2px"/>
</body></html>
In this fiddle, I've modified your code to center the text by using the actual bounding box properties of the measureText function.
fontSize = fontSize * width / (measure.actualBoundingBoxLeft + measure.actualBoundingBoxRight);
const xPos = x + (width / 2) + ((measure.actualBoundingBoxLeft - measure.actualBoundingBoxRight) / 2);
const yPos = y + (height / 2) + ((measure.actualBoundingBoxAscent - measure.actualBoundingBoxDescent) / 2);
A more accurate width of the text is the sum of the right and left bounding box sizes and the offset of the true center from the rendered point can be calculated as shown above.
This is necessary because the font, and even the specific characters used, can cause the actual bounding box to not be centred on the render point.
*Edit I made a mistake when I wrote my previous answer and updated the font size in the wrong way. I was tired

Drawing multiple offscreen canvases into onscreen canvas

I am having an issue when I'm trying to render multiple offscreen canvases into onscreen canvas. I do get one offscreen canvas rendered but the problem is that there should be two other rendered before. In other words, only last canvas is rendered. The expected result would be three overlapping rectangles (or squares :) in red, green and blue. Here's the code:
function rectangle(color) {
var offScreenCanvas = document.createElement('canvas');
var offScreenCtx = offScreenCanvas.getContext('2d');
var width = offScreenCanvas.width = 150;
var height = offScreenCanvas.height = 150;
switch(color) {
case 1:
offScreenCtx.fillStyle='rgb(255,0,0)';
break;
case 2:
offScreenCtx.fillStyle='rgb(0,255,0)';
break;
case 3:
offScreenCtx.fillStyle='rgb(0,0,255)';
break;
}
offScreenCtx.fillRect(0,0,width,height);
return offScreenCanvas;
}
function draw(offScreenCanvas, x , y) {
var canvas = document.getElementById('canvas')
var ctx = canvas.getContext('2d');
var width = canvas.width = window.innerWidth;
var height = canvas.height = window.innerHeight;
ctx.drawImage(offScreenCanvas, x, y);
}
var images = [];
var color = 1;
for (var i=0; i<3; i++) {
var img = new rectangle(color);
images.push(img);
color++;
}
var x = 0;
var y = 0;
for (var i = 0; i < images.length; i++) {
draw(images[i], x, y);
x += 100;
y += 100;
}
I did some searching and it seems that I'm not the first with this issue, but I could not get this working properly.
Setting canvas height or width clears the canvas.
The problem with your code is that you are causing the onscreen canvas to be cleared when you set it size in the function draw
Setting the canvas size, even if that size is the same, will cause the canvas context to reset and clear the canvas. All the other canvases are rendered, but erased when you set the onscreen canvas size.
Your draw function
function draw(offScreenCanvas, x , y) {
var canvas = document.getElementById('canvas')
var ctx = canvas.getContext('2d');
// The cause of the problem ===================================
// Either one of the following lines will clear the canvas
var width = canvas.width = window.innerWidth;
var height = canvas.height = window.innerHeight;
//=============================================================
ctx.drawImage(offScreenCanvas, x, y);
}
To avoid this just set the canvas size once. If you need to resize the canvas and keep its content you first need to create a copy of the canvas, then resize it, then render the copy back to the original.
Demo shows 5 offscreen canvases being rendered onto one onscreen canvas.
const colours = ['#f00', '#ff0', '#0f0', '#0ff', '#00f'];
const ctx = can.getContext('2d');
can.width = innerWidth - 4; // sub 4 px for border
can.height = innerHeight - 4;
function createCanvas(color, i) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 150;
canvas.height = 150;
ctx.font = "24px arial";
ctx.fillStyle = color;
ctx.fillRect(0, i * 30, canvas.width, 30);
ctx.fillStyle = "black";
ctx.fillText("Canvas "+i,10,(i + 0.75) * 30);
return canvas;
}
colours.forEach((c, i) => {
ctx.drawImage(createCanvas(c, i), 0, 0);
})
canvas {
border: 2px solid black;
position : absolute;
top : 0px;
left : 0px;
}
<canvas id="can"></canvas>

Use Clip Function to with Gradient Effect with JavaScript on Canvas

I'm trying to use the clip() function in canvas to create this effect, as pictured: there is a background image, and when your mouse hover on it, part of the image is shown. I got it to work as a circle, but I want this gradient effect you see the picture. How do I achieve that?
<!DOCTYPE HTML>
<html>
<head>
<link rel="stylesheet" type="text/css" href="./assets/stylesheet/normalize.css">
<link rel="stylesheet" type="text/css" href="./assets/stylesheet/style.css">
</head>
<body>
<canvas id="canvas" width="2000" height="1200"></canvas>
<script>
var can = document.getElementById('canvas');
var ctx = can.getContext('2d');
can.addEventListener('mousemove', function(e) {
var mouse = getMouse(e, can);
redraw(mouse);
}, false);
function redraw(mouse) {
console.log('a');
can.width = can.width;
ctx.canvas.width = window.innerWidth;
ctx.canvas.height = window.innerHeight;
ctx.drawImage(img, 0, 0);
ctx.beginPath();
ctx.rect(0,0,2000,1200);
ctx.arc(mouse.x, mouse.y, 200, 0, Math.PI*2, true)
ctx.clip();
ctx.fillRect(0,0,2000,1200);
}
var img = new Image();
img.onload = function() {
redraw({x: 0, y: 0})
}
img.src = 'http://placekitten.com/2000/1000';
function getMouse(e, canvas) {
var element = canvas,
offsetX = 0,
offsetY = 0,
mx, my;
// Compute the total offset. It's possible to cache this if you want
if (element.offsetParent !== undefined) {
do {
offsetX += element.offsetLeft;
offsetY += element.offsetTop;
} while ((element = element.offsetParent));
}
mx = e.pageX - offsetX;
my = e.pageY - offsetY;
return {
x: mx,
y: my
};
}
</script>
USING a RADIAL gradient
There are many ways to do that but the simplest is a gradient with an alpha.
First you need to define the size of the circle you wish to show.
var cirRadius = 300;
Then the location (canvas coordinates) where this circle will be centered
var posX = 100;
var posY = 100;
Now define the rgb colour
var RGB = [0,0,0] ; // black
Then an array of alpha values to define what is transparent
var alphas = [0,0,0.2,0.5,1]; // zero is transparent;
Now all you do is render the background image
// assume ctx is context and image is loaded
ctx.drawImage(image, 0, 0, ctx.canvas.width, ctx.canvas.height); // fill the canvas
Then create the gradient with it centered at the position you want and the second circle at the radius you want. The first 3 numbers define the center and radius of the start of the gradient, the last 3 define the center and radius of the end
var grad = ctx.createRadialGradient(posX,posY,0,posX,posY,cirRadius);
Now add the colour stops using the CSS color string rgba(255,255,255,1) where the last is the alpha value from 0 to 1.
var len = alphas.length-1;
alphas.forEach((a,i) => {
grad.addColorStop(i/len,`rgba(${RGB[0]},${RGB[1]},${RGB[2]},${a})`);
});
or for legacy browsers that do not support arrow functions or template strings
var i,len = alphas.length;
for(i = 0; i < len; i++){
grad.addColorStop(i / (len - 1), "rgba(" + RGB[0] + "," + RGB[1] + "," + RGB[2] + "," + alphas[i] + ")");
}
Then set the fill style to the gradient
ctx.fillStyle = grad;
then just fill a rectangle covering the image
ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height);
And you are done.
By setting the position with via a mouse event and then doing the above steps 60times a second using window.requestAnimationFrame you can get the effect you are looking for in real time.
Here is an example
// create a full screen canvas
var canvas = document.createElement("canvas");
canvas.style.position = "absolute";
canvas.style.left = "0px";
canvas.style.top = "0px";
canvas.style.zIndex = 10;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);
// var to hold context
var ctx;
// load an image
var image = new Image();
image.src = "https://i.stack.imgur.com/C7qq2.png?s=328&g=1";
// add resize event
var resize = function(){
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
}
// add mouse event. Because it is full screen no need to bother with offsets
var mouse = function(event){
posX = event.clientX;
posY = event.clientY;
}
// incase the canvas size is changed
window.addEventListener("resize",resize);
// listen to the mouse move
canvas.addEventListener("mousemove",mouse)
// Call resize as that gets our context
resize();
// define the gradient
var cirRadius = 300;
var posX = 100; // this will be set by the mouse
var posY = 100;
var RGB = [0,0,0] ; // black any values from 0 to 255
var alphas = [0,0,0.2,0.5,0.9,0.95,1]; // zero is transparent one is not
// the update function
var update = function(){
if(ctx){ // make sure all is in order..
if(image.complete){ // draw the image when it is ready
ctx.drawImage(image,0,0,canvas.width,canvas.height)
}else{ // while waiting for image clear the canvas
ctx.clearRect(0,0,canvas.width,canvas.height);
}
// create gradient
var grad = ctx.createRadialGradient(posX,posY,0,posX,posY,cirRadius);
// add colour stops
var len = alphas.length-1;
alphas.forEach((a,i) => {
grad.addColorStop(i/len,`rgba(${RGB[0]},${RGB[1]},${RGB[2]},${a})`);
});
// set fill style to gradient
ctx.fillStyle = grad;
// render that gradient
ctx.fillRect(0,0,canvas.width,canvas.height);
}
requestAnimationFrame(update); // keep doing it till cows come home.
}
// start it all happening;
requestAnimationFrame(update);

After the width or height of a canvas element changed, the font for its 2d context set to default value

var cvs = Damoo.dom.createElement('canvas'),
ctx = cvs.getContext('2d');
ctx.font = f.value;
cvs.width = ctx.measureText("text").width;
cvs.height = f.size * 1.2;
//now the font value set to default value, you must reset it
ctx.font = f.value;
So anyone knows is this a bug or by design?
The environment is latest Chrome
When I try save and restore, the code is
var cvs = Damoo.dom.createElement('canvas'),
ctx = cvs.getContext('2d');
ctx.font = f.value;
cvs.save();
cvs.width = ctx.measureText("text").width;
cvs.height = f.size * 1.2;
//now the font value set to default value, you must reset it
//ctx.font = f.value;
cvs.restore();
ctx.save() and ctx.restore() ignore the font value. Instead of restore, reset your font (They probably forgot to include it because any other explanation is pointless.)
Solution:
ctx.font = f.value;
...measureText and adjust canvas size
ctx.font = f.value;

HTML 5 Canvas | Resize height of canvas according to text line count?

Is there a way to resize the height of a canvas according to text line count? The text will reside at bottom of canvas. An image will also be drawn into canvas at top-left most position of canvas, at same time of text, so the text will always start at a fixed location on canvas just under the image. I just want for the canvas to resize vertically according to text line-count, not while I am typing, only after I click a button after the text has been typed in a textarea.
I'm using this for text:
...
var str = $('#TEXTAREA').val();
var splittext = $.map(str.split(" "), function (t) { // Split words > than 40 chars
return t.match(/[\s\S]{1,40}/g) || [];
}).join(" ");
qrc.font = 'normal normal 14px monospace';
qrc.textAlign = 'center';
qrc.fillStyle = '#000';
wrapText(qrc, splittext, x, y, maxWidth, lineHeight);
function wrapText(context, text, x, y, maxWidth, fontSize, fontFace){
var words = text.split(' ');
var line = '';
var lineHeight = fontSize;
context.font=fontSize+" "+fontFace;
for(var n = 0; n < words.length; n++){
var testLine = line + words[n] + ' ';
var metrics = context.measureText(testLine);
var testWidth = metrics.width;
if(testWidth > maxWidth) {
context.fillText(line, x, y);
line = words[n] + ' ';
y += lineHeight;
if(++lineCount>6){return(y);} // Change to "Add 14px to canvas height at each line count" ?
}
else {
line = testLine;
}
}
context.fillText(line, x, y);
return(y);
}
Currently, this code doesn't print text beyond 6 lines of text. I don't know where in the code to modify / add (see comments in code). Any hints?
UPDATE w/ FIDDLE
Here's a fiddle to experiment with. Please use that fiddle in your answer. Read TODO comment in fiddle. I'm stumped, but we're nearly thar! Thx for input. http://jsfiddle.net/uL84x7vw/12/
UPDATE w/ FIDDLE
This fiddle is same as previous but moves the image placeholder code to bottom of fiddle code so it doesn't get cleared. It's looking better but all lines of text except last line of text are still being wiped out. Obviously some code missing. Thx for input. http://jsfiddle.net/uL84x7vw/15/
As I understand you want your text dynamically appear and be resized in canvas. Use onchange event to track the number of lines of your text and dynamically manipulate canvas. Each time you write character it will be already on canvas. Use canvas.width = canvas.width + 14;
If you're using jquery:
var canvas = $('#yourCanvas');
canvas.height(canvas.height()+14);
If you're not (and you are using height="" attribute):
var canvas = document.getElementById('yourCanvas');
canvas.height = canvas.height+14;
If you're using css to style the height:
var canvas = document.getElementById('yourCanvas');
canvas.style.height = (parseInt(canvas.style.height.substring(0,canvas.style.height.length-2))+14)+"px";
UPDATE - FINAL
DEMO
Finally got it working! The text displays under the image placeholder and resizes (vertically) the canvas at each addition or deletion of line of text. The split function splits long words exceeding canvas width (or whatever width prescribed). Using monospace font for fixed-width character display, but you can try other types of fonts.
Copying a canvas to image and back saved the day! :
function showCanvas(){
var elem = document.getElementById('myCanvas');
var wd = 200;
var ht = 110;
ctx = elem.getContext('2d');
ctx.canvas.width = wd;
ctx.canvas.height = ht;
ctx.fillStyle = '#FFF'; // Text area BG color.
ctx.fillRect(0,0,wd,ht);
var str = $('#textArea').val();
var maxWidth = 190;
var lineHeight = 14;
var x = 100; // Since text alignment is centered.
var y = 104; // Start position of text (under image placeholder).
var lineCount = 1;
var splittext = $.map(str.split(" "), function (t) {
return t.match(/[\s\S]{1,20}/g) || [];
}).join(" ");
ctx.font = 'normal normal 14px monospace'; // Monospace fonts have same widths.
ctx.textAlign = 'center'; // Choose text alignment, change var x accordingly.
ctx.fillStyle = '#000';
wrapText(ctx, splittext, x, y, maxWidth, lineHeight);
function wrapText(context, text, x, y, maxWidth, fontSize){
var words = text.split(' ');
var line = '';
var lineHeight = fontSize;
context.font=fontSize+" ";
for(var n = 0; n < words.length; n++){
var testLine = line + words[n] + ' ';
var metrics = context.measureText(testLine);
var testWidth = metrics.width;
if(testWidth > maxWidth) {
context.fillText(line, x, y);
line = words[n] + ' ';
y += lineHeight;
if(++lineCount>1){ // If more than one line of text.
// Start - Canvas copy:
/* Only use drawImage for copying Canvas contents since getImageData()
is pixel data extraction requiring more processing time and cpu overhead. */
var backCanvas = document.createElement('canvas');
backCanvas.width = elem.width;
backCanvas.height = elem.height;
var backCtx = backCanvas.getContext('2d');
backCtx.drawImage(elem, 0,0);
// End - Canvas copy.
elem.height = elem.height+(lineHeight); // Canvas resizes vertically.
ctx.font = 'normal normal 14px monospace';
ctx.textAlign = 'center'; // Choose text alignment, change var x accordingly.
ctx.fillStyle = '#000';
ctx.drawImage(backCanvas, 0,0); // Paste canvas copy into source canvas.
}
}
else {
line = testLine;
}
}
context.fillText(line, x, y);
return(y);
}
// Create image placeholder.
ctx.fillStyle = 'red';
ctx.fillRect(0,0,200,90);
}
Feel free to comment on streamlining code where applicable. Thx for input.

Categories