How can I center a letter in circle html canvas? - javascript

So far, to do the centering, I am using the following two lines of code:
ctx.textAlign="center";
ctx.textBaseline = "middle";
This almost does the job, but some characters like "g" and "y" are not completely centered. How can I make sure that all of them are supported? Here is a JSbin that shows that the majority of characters like "g" is below the center line.
Expectation:
Reality:
To make my "expectation" work, I subtract 15px from the y value of the letter, but this messes up small letters like "a" and makes them go outside of the bounds on the top.

Measuring text.
One way is to render the character and then scan the pixels to find the extent, top, bottom, left, and right, to find the real center of the character.
This is an expensive process so added to that you would store the results of previous measurements in a map and return those results for the same character and font.
The example below creates the object charSizer. You set a font charSizer.font = "28px A font" then you can get the information regarding any character. charSizer.measure(char) which returns an object containing information regarding the characters dimensions.
You can measure characters in production and serve the information to the page to reduce client side processing but you will need to target each browser as they all render text differently.
Example
The example has instructions. The left canvas show char render to normal center using ctx.textAlign = "center" and ctx.textBaseline = "middle". Also included are color codded lines to show extent, center, bounds center, and weighted center. The middle canvas draw the char in circle using bounds center and the right canvas uses weighted center.
This is an example only, untested and not up to production quality.
const charSizer = (() => {
const known = new Map();
var w,h,wc,hc;
const workCan = document.createElement("canvas");
const ctx = workCan.getContext("2d");
var currentFont;
var fontHeight = 0;
var fontId = "";
function resizeCanvas(){
wc = (w = workCan.width = fontHeight * 2.5 | 0) / 2;
hc = (h = workCan.height = fontHeight * 2.5 | 0) / 2;
ctx.font = currentFont;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "black";
}
function measure(char){
const info = {
char,
width : ctx.measureText(char).width,
top : null,
left : w,
right : 0,
bottom : 0,
weightCenter : { x : 0, y : 0 },
center : { x : 0, y : 0 },
offset : { x : 0, y : 0 },
wOffset : { x : 0, y : 0 },
area : 0,
width : 0,
height : 0,
}
ctx.clearRect(0,0,w,h);
ctx.fillText(char,wc,hc);
const pixels8 = ctx.getImageData(0,0,w,h).data;
const pixels = new Uint32Array(pixels8.buffer);
var x,y,i;
i = 0;
for(y = 0; y < h; y ++){
for(x = 0; x < w; x ++){
const pix = pixels[i++];
if(pix){
const alpha = pixels8[(i<<2)+3];
info.bottom = y;
info.right = Math.max(info.right, x);
info.left = Math.min(info.left, x);
info.top = info.top === null ? y : info.top;
info.area += alpha;
info.weightCenter.x += (x - wc) * (alpha/255);
info.weightCenter.y += (y - hc) * (alpha/255);
}
}
}
if(info.area === 0){
return {empty : true};
}
info.area /= 255;
info.weightCenter.x /= info.area;
info.weightCenter.y /= info.area;
info.height = info.bottom - info.top + 1;
info.width = info.right - info.left + 1;
info.center.x = info.left + info.width / 2;
info.center.y = info.top + info.height / 2;
info.offset.x = wc - info.center.x;
info.offset.y = hc - info.center.y;
info.wOffset.x = -info.weightCenter.x;
info.wOffset.y = -info.weightCenter.y;
info.top -= hc;
info.bottom -= hc;
info.left -= wc;
info.right -= wc;
info.center.x -= wc;
info.center.y -= hc;
return info;
}
const API = {
set font(font){
currentFont = font;
fontHeight = Number(font.split("px")[0]);
resizeCanvas();
fontId = font;
},
measure(char){
var info = known.get(char + fontId);
if(info) { return {...info} } // copy so it is save from change
info = measure(char);
known.set(char + fontId,info);
return info;
}
}
return API;
})()
//==============================================================================
//==============================================================================
// Demo code from here down not part of answer code.
const size = 160;
const sizeh = 80;
const fontSize = 120;
function line(x,y,w,h){
ctx.fillRect(x,y,w,h);
}
function hLine(y){ line(0,y,size,1) }
function vLine(x){ line(x,0,1,size) }
function circle(ctx,col = "red",x= sizeh,y = sizeh,r = sizeh*0.8,lineWidth = 2) {
ctx.lineWidth = lineWidth;
ctx.strokeStyle = col;
ctx.beginPath();
ctx.arc(x,y,r,0,Math.PI * 2);
ctx.stroke();
}
const ctx = canvas.getContext("2d");
const ctx1 = canvas1.getContext("2d");
const ctx2 = canvas2.getContext("2d");
canvas.width = size;
canvas.height = size;
canvas1.width = size;
canvas1.height = size;
canvas2.width = size;
canvas2.height = size;
canvas.addEventListener("click",nextChar);
canvas1.addEventListener("click",nextFont);
ctx.font = "20px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Click this canvas", sizeh,sizeh-30);
ctx.fillText("cycle", sizeh,sizeh);
ctx.fillText("characters", sizeh,sizeh + 30);
ctx1.font = "20px Arial";
ctx1.textAlign = "center";
ctx1.textBaseline = "middle";
ctx1.fillText("Click this canvas", sizeh,sizeh - 30);
ctx1.fillText("cycle", sizeh,sizeh);
ctx1.fillText("fonts", sizeh,sizeh + 30);
charSizer.font = "128px Arial";
ctx1.textAlign = "center";
ctx1.textBaseline = "middle";
ctx2.textAlign = "center";
ctx2.textBaseline = "middle";
const chars = "\"ABCDQWZ{#pqjgw|/*";
const fonts = [
fontSize+"px Arial",
fontSize+"px Arial Black",
fontSize+"px Georgia",
fontSize+"px Impact, Brush Script MT",
fontSize+"px Rockwell Extra Bold",
fontSize+"px Franklin Gothic Medium",
fontSize+"px Brush Script MT",
fontSize+"px Comic Sans MS",
fontSize+"px Impact",
fontSize+"px Lucida Sans Unicode",
fontSize+"px Tahoma",
fontSize+"px Trebuchet MS",
fontSize+"px Verdana",
fontSize+"px Courier New",
fontSize+"px Lucida Console",
fontSize+"px Georgia",
fontSize+"px Times New Roman",
fontSize+"px Webdings",
fontSize+"px Symbol",];
var currentChar = 0;
var currentFont = 0;
var firstClick = true;
function nextChar(){
if(firstClick){
setCurrentFont();
firstClick = false;
}
ctx.clearRect(0,0,size,size);
ctx1.clearRect(0,0,size,size);
ctx2.clearRect(0,0,size,size);
var c = chars[(currentChar++) % chars.length];
var info = charSizer.measure(c);
if(!info.empty){
ctx.fillStyle = "red";
hLine(sizeh + info.top);
hLine(sizeh + info.bottom);
vLine(sizeh + info.left);
vLine(sizeh + info.right);
ctx.fillStyle = "black";
hLine(sizeh);
vLine(sizeh);
ctx.fillStyle = "red";
hLine(sizeh + info.center.y);
vLine(sizeh + info.center.x);
ctx.fillStyle = "blue";
hLine(sizeh + info.weightCenter.y);
vLine(sizeh + info.weightCenter.x);
ctx.fillStyle = "black";
circle(ctx,"black");
ctx.fillText(c,sizeh,sizeh);
ctx1.fillStyle = "black";
circle(ctx1);
ctx1.fillText(c,sizeh + info.offset.x,sizeh+ info.offset.y);
ctx2.fillStyle = "black";
circle(ctx2,"blue");
ctx2.fillText(c,sizeh + info.wOffset.x, sizeh + info.wOffset.y);
}
}
function setCurrentFont(){
fontUsed.textContent = fonts[currentFont % fonts.length];
charSizer.font = fonts[currentFont % fonts.length];
ctx.font = fonts[currentFont % fonts.length];
ctx2.font = fonts[currentFont % fonts.length];
ctx1.font = fonts[(currentFont ++) % fonts.length];
}
function nextFont(){
setCurrentFont();
currentChar = 0;
nextChar();
}
canvas { border : 2px solid black; }
.red {color :red;}
.blue {color :blue;}
<canvas id="canvas"></canvas><canvas id="canvas1"></canvas><canvas id="canvas2"></canvas><br>
Font <span id="fontUsed">not set</span> [center,middle] <span class=red>[Spacial center]</span> <span class=blue> [Weighted center]</span><br>
Click left canvas cycles char, click center to cycle font. Not not all browsers support all fonts

Related

Placing a letter randomly in a canvas offset by a % from border

I have this canvas where I place a letter randomly within the canvas.
var w = Math.random() * canvas.width;
var h = Math.random() * canvas.height;
drawRandomCircle(canvas,w,h);
function drawRandomCircle(canvas,w,h)
{
var fontSize = '35';
var ctx = canvas.getContext("2d");
var color = 'rgba(245, 66, 66,1.0)';
ctx.fillStyle = color;
ctx.font = fontSize + 'pt Arial';
ctx.fillText('O', w, h);
}
The results:
I would like to improve further on the function to include an offset in % from the canvas boundary to limit where the letter will appear.
The results would be similar to something similar to this.
Any ideas?
You need to take into account the 10% on the borders.
Try the following which uses this principle... but also remember that the co-ordinates for the canvas are top-left based... but when you do the font it will go UP (not down) so you have to take that into account as well.
var canvas = document.getElementsByTagName("canvas")[0];
var fontSizePx = 35;
// Get 10% of the width/height
var cw = (canvas.width / 10);
var ch = (canvas.height / 10);
// Get 80% of the width/height but minus the size of the font
var cw80 = (cw * 8) - fontSizePx;
var ch80 = (ch * 8) - fontSizePx;
for(var i = 0; i < 10; i++) {
// Get random value within center 80%
var w = (Math.random() * cw80) + cw;
// Add on the size of the font to move it down
var h = (Math.random() * ch80) + ch + fontSizePx;
drawRandomCircle(canvas,w,h);
}
function drawRandomCircle(canvas,w,h) {
var ctx = canvas.getContext("2d");
var color = 'rgba(245, 66, 66,1.0)';
ctx.fillStyle = color;
ctx.font = fontSizePx.toString() + 'px Arial';
ctx.fillText('O', w, h);
}
canvas {
border:1px solid black;
}
<canvas></canvas>

canvas is stretching when changing device type in dev tool

I have next canvas with some content:
class ExpectedLifespanRound {
constructor() {
// max lifespan that is possible
this.lifespanMax = 125
// expected lifespan block
this.expLsBlock = document.getElementById("expected-lifespan-block")
// set style for the block
this.style = this.expLsBlock.style
this.style.padding = "20px"
this.style.display = "flex"
this.style.width = "320px"
this.style.height = "406px"
this.style.borderRadius = "20px"
this.style.backgroundColor = "white"
// set canvas style
this.can = this.initCanvas("expected-lifespan-block")
this.can.style.backgroundColor = "white";
// - 40 is padding
this.can.style.width = this.expLsBlock.outerWidth - 40 + "px"
this.can.style.height =this.expLsBlock.outerHeight - 40 + "px"
this.can.style.marginLeft = "8px"
this.expLsBlock.appendChild(this.can);
this.ctx = this.can.getContext("2d")
// set gradient for arc
this.gradient = this.ctx.createLinearGradient(320, 0, 0, 406);
this.gradient.addColorStop("0.3", "#ABB8FF")
this.gradient.addColorStop("0.7" ,"#ABF0FF")
}
clearCanvas() {
// clear canvas
this.ctx.clearRect(0, 0, 320, 406)
this.ctx.beginPath();
this.ctx.globalCompositeOperation = "source-over";
this.ctx.globalAlpha = 1;
this.ctx.closePath();
}
create(years) {
// title
this.ctx.save()
this.clearCanvas()
this.ctx.fillStyle = "black"
this.ctx.font = "bolder 18px Arial";
this.ctx.fillText("Ожидаемая", 100, 30)
this.ctx.fillText("продолжительность жизни", 32, 56)
this.ctx.restore()
// arc
this.ctx.save()
this.ctx.fillStyle = this.gradient
this.ctx.strokeStyle = this.gradient
this.ctx.lineWidth = 35
this.ctx.arc(150, 250, 125, Math.PI - (1.5 * Math.PI/this.lifespanMax)*years, 1.5 * Math.PI)
this.ctx.font = "bold 55px Arial"
var x = 120
if (years >= 100) {x = 105}
this.ctx.fillText(years, x, 245)
this.ctx.font = "bold 36px Arial"
this.ctx.fillText("лет", 120, 285)
this.ctx.stroke()
this.ctx.restore()
}
refresh(years) {
this.create(years)
}
initCanvas(blockForCanvas) {
// create high dpi canvas and put it in the block
// return pixel ratio
var getRatio = function () {
var ctx = document.createElement("canvas").getContext("2d");
var dpr = window.devicePixelRatio || 1;
var bsr =
ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio ||
1;
return dpr / bsr;
};
// return high dots per inch canvas
var createHiDPICanvas = function (w, h) {
var ratio = getRatio();
var chart_container = document.getElementById(blockForCanvas);
var can = document.createElement("canvas");
can.style.backgroundColor = "white";
can.width = 320 * ratio;
can.height = 406 * ratio;
can.style.width = w + "px";
can.style.height = h + "px";
can.getContext("2d").setTransform(ratio, 0, 0, ratio, 0, 0);
chart_container.appendChild(can);
return can;
}
var canvas = createHiDPICanvas(this.scaleWidth, this.scaleHeight)
return canvas
}
}
var expLsRound = new ExpectedLifespanRound()
expLsRound.create(95)
body {
background-color:grey;
}
<div id="expected-lifespan-block"></div>
It is ok on my 1920x1080 screen, but when I switch type to mobile (line iphone 6, ipad or any other) - it is strating to stretch in not appropriate way. You can test it in chrome dev tools, or take a look on the pictures I tave taken:
normal:
stretched after switching to another device using dev tools:
How to avoid such a stretching and let this canvas be the same size no depending what device screen user has?
Just needed to replace 2 functions out if innerCanvas function and it does not stretch anymore

Making a Graph/Curve with Javascript and HTML

I am trying to make a program that you can can input numbers into, and it creates a graph based off of those (like Desmos, but the numbers you input don't need to be formatted like (1, 0) and you can control the spacing of the numbers). I am coding this in Javascript, and HTML for the actual inputs and canvas.
I have tried using this website as a foundation to help me create the curve.
Here is the combined Javascript and HTML code:
<!DOCTYPE html>
<html>
<body>
<form>
<label></label>
<input id="numbers" value="Numbers">
<button onClick="refresh()">Submit</button>
</form>
<form>
<label></label>
<input id="spacing" value="Spacing">
<button onClick="refresh()">Submit</button>
</form>
<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;"></canvas>
<script>
refresh()
numbers = document.getElementById("numbers").value;
function refresh(){
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.bezierCurveTo(numbers);
ctx.stroke();
}
</script>
</html>
Please note that I know that the canvas does not work (the variable numbers isn's working, any help?), but my main concern is that this code doesn't create a graph like I want it to. I want to type in numbers, and have it plot invisible points x numbers above the "ground" (the bottom of the canvas) and then use bezier to make a curve go through those invisible points (don't make the points, that was the only way I could think of explaining it).
For my internship, I was asked to develop a lightweight web application for some of our internal operations. One of the functions I wanted to include was a graph/chart that plotted Sales data for a selected employee. I also wanted to be able to change the resolution of the chart's x-axis such that I could view Sales by day, week, month, quarter, or year.
Here's what I came up with using just basic JavaScript and HTML Canvas.
Regarding the fix_dpi() function (that may be confusing to new coders, like myself), here was an article explaining what it is and why you will want to adjust for it: https://medium.com/wdstack/fixing-html5-2d-canvas-blur-8ebe27db07da .
Also, note that my drawYTicks() function has a special case for the "50K" and "500K" labels. If you need something a little more general, just copy the code of from drawXTicks() and make the minor changes necessary for it's domain.
Code:
<script>
let canvas = document.getElementById('canvas'), ctx = canvas.getContext('2d'), dpi = window.devicePixelRatio;
let xOrig = canvas.width * 0.15;
let yOrig = canvas.height * 0.85;
let xLen = canvas.width * 0.7;
let yLen = canvas.height * 0.7;
function fix_dpi() {
let style = {
height() {
return +getComputedStyle(canvas).getPropertyValue('height').slice(0, -2);
},
width() {
return +getComputedStyle(canvas).getPropertyValue('width').slice(0, -2);
}
}
canvas.setAttribute('width', style.width() * dpi);
canvas.setAttribute('height', style.height() * dpi);
xOrig = canvas.width * 0.15;
yOrig = canvas.height * 0.85;
xLen = canvas.width * 0.7;
yLen = canvas.height * 0.7;
}
function draw() {
fix_dpi(); //fixes dpi for sharper image...
var dataSet = // int[][] containing the data.
var xLabs = // a string[] containing x-labels.
var yLabs = // a string[] containing y-labels.
var range = // an integer expressing the top of your range (x-axis);
var domain = // an integer expressing the top of your domain (y-axis)
console.log(xLabs);
console.log(xLabs.length);
console.log(yLabs);
console.log(yLabs.length);
drawChartTitle("Employee (Total) Sales History");
fillDataSpace(dataSet, range, domain, '#3F3');
drawDataLine(dataSet, range, domain, '#000');
drawDataPoints(dataSet, range, domain, '#F00');
drawXTicks(xLabs);
drawYTicks(yLabs);
drawAxes();
}
function drawChartTitle(title) {
ctx.save();
ctx.fillStyle = '#000';
ctx.font = '24px Times New Roman';
ctx.textAlign = 'center';
ctx.fillText(title, canvas.width / 2, canvas.height * 0.1);
ctx.restore();
}
function drawAxes(color='#000') {
ctx.save();
ctx.beginPath();
ctx.strokeStyle = color;
ctx.moveTo(xOrig, (yOrig - yLen));
ctx.lineTo(xOrig, yOrig);
ctx.lineTo((xOrig + xLen),yOrig);
ctx.stroke();
ctx.restore();
}
function drawYTicks(yLabs) {
//Note: two of the ticks have to be handled separately
//for this program, 50K and 500K.
ctx.save();
let tickLen = canvas.height / 50;
let yDivs = yLen / (yLabs.length - 1);
let tickX = xOrig - tickLen / 2;
let labX = xOrig - tickLen;
ctx.beginPath();
ctx.strokeStyle = '#000';
ctx.fillStyle = '#000';
ctx.font = '12px Times New Roman';
ctx.textAlign = 'right';
if (yLabs.length >= 4) {
yDivs = yLen / (yLabs.length - 3);
//Draw million dollar ticks and labels
for (let i = 3; i < yLabs.length; i++) {
let tickY = yOrig - yDivs * (i-2);
ctx.moveTo(tickX, tickY);
ctx.lineTo((tickX + tickLen), tickY);
}
for (let i = 3; i < yLabs.length; i++) {
let labY = yOrig - yDivs * (i-2) + 4;
ctx.fillText(yLabs[i], labX, labY);
}
//Draw 50K and 500K
let fifty = yOrig - yDivs * 0.1;
ctx.moveTo(tickX, fifty);
ctx.lineTo((tickX + tickLen), fifty);
ctx.fillText(yLabs[1], labX, fifty + 4);
let fHundredK = yOrig - yDivs * 0.5;
ctx.moveTo(tickX, fHundredK);
ctx.lineTo((tickX + tickLen), fHundredK);
ctx.fillText(yLabs[2], labX, fHundredK + 4);
}
else if (yLabs.length == 3) {
yDivs = yLen / (yLabs.length - 2);
//Draw 50K and 500K only
let fifty = yOrig - yDivs * 0.1;
ctx.moveTo(tickX, fifty);
ctx.lineTo((tickX + tickLen), fifty);
ctx.fillText(yLabs[1], labX, fifty + 4);
let fHundredK = yOrig - yDivs;
ctx.moveTo(tickX, fHundredK);
ctx.lineTo((tickX + tickLen), fHundredK);
ctx.fillText(yLabs[2], labX, fHundredK + 4);
}
else {
//Draw 50K only
let fifty = yOrig - yDivs;
ctx.moveTo(tickX, fifty);
ctx.lineTo((tickX + tickLen), fifty);
ctx.fillText(yLabs[1], labX, fifty + 4);
}
let zero = yOrig;
ctx.moveTo(tickX, zero);
ctx.lineTo((tickX + tickLen), zero);
ctx.fillText(yLabs[0], labX, zero + 4);
ctx.stroke();
ctx.restore();
}
function drawXTicks(xLabs) {
ctx.save();
let tickLen = canvas.height / 50;
let xDivs = xLen / (xLabs.length - 1);
ctx.beginPath();
ctx.strokeStyle = '#000';
for (let i = 0; i < xLabs.length; i++) {
let tickX = xOrig + xDivs * i;
let tickY = yOrig + tickLen / 2;
ctx.moveTo(tickX, tickY);
ctx.lineTo(tickX, (tickY - tickLen));
}
ctx.stroke();
ctx.restore();
for (let i = 0; i < xLabs.length; i++) {
ctx.save();
ctx.fillStyle = '#000';
ctx.font = '12px Times New Roman';
ctx.textAlign = 'right';
ctx.translate((canvas.width*0.15) + (xDivs * i), canvas.height*0.15);
ctx.rotate(-Math.PI / 4);
let labY = canvas.height * 0.52;
let labX = -canvas.width * 0.38;
ctx.fillText(xLabs[i], labX, labY);
ctx.restore();
}
}
function drawDataPoints(coords, maxX, maxY, color='#000') {
ctx.save();
if (coords.length >= 2) {
let xScale = xLen / maxX;
let yScale = yLen / maxY;
let pointCir = canvas.height / 200;
ctx.beginPath();
for (let i = 0; i < coords.length; i++) {
let xp = xOrig + coords[i][0] * xScale;
let yp = yOrig - coords[i][1] * yScale;
ctx.moveTo(xp, yp);
ctx.arc(xp, yp, pointCir, 0, Math.PI * 2);
}
ctx.fillStyle = color;
ctx.fill();
ctx.strokeStyle = color;
ctx.stroke();
}
else {
ctx.fillStyle = '#000';
ctx.font = '24px Sans-Serif';
ctx.fillText('There is no data to display', (xOrig + xLen * 0.3), (yOrig - yLen * 0.5));
}
ctx.restore();
}
function drawDataLine(coords, maxX, maxY, color='#000') {
ctx.save();
if (coords.length >= 2) {
let xScale = xLen / maxX;
let yScale = yLen / maxY;
let xp = xOrig + coords[0][0] * xScale;
let yp = yOrig - coords[0][1] * yScale;
ctx.beginPath();
ctx.moveTo(xp, yp);
for (let i = 1; i < coords.length; i++) {
xp = xOrig + coords[i][0] * xScale;
yp = yOrig - coords[i][1] * yScale;
ctx.lineTo(xp, yp);
}
ctx.strokeStyle = color;
ctx.stroke();
}
else {
ctx.fillStyle = '#000';
ctx.font = '24px Sans-Serif';
ctx.fillText('There is no data to display', (xOrig + xLen * 0.3), (yOrig - yLen * 0.5));
}
ctx.restore();
}
function fillDataSpace(coords, maxX, maxY, color = '#00F') {
ctx.save();
if (coords.length >= 2) {
let xScale = xLen / maxX;
let yScale = yLen / maxY;
let xp = xOrig + coords[0][0] * xScale;
let yp = yOrig - coords[0][1] * yScale;
var lingrad = ctx.createLinearGradient(xOrig, yOrig - yLen, xOrig + xLen, yOrig);
lingrad.addColorStop(0, '#FFF');
lingrad.addColorStop(0.5, color);
lingrad.addColorStop(1, '#FFF');
ctx.beginPath();
ctx.moveTo(xp, yp);
for (let i = 1; i < coords.length; i++) {
xp = xOrig + coords[i][0] * xScale;
yp = yOrig - coords[i][1] * yScale;
ctx.lineTo(xp, yp);
}
ctx.lineTo(xOrig + xLen, yOrig);
ctx.lineTo(xOrig, yOrig);
ctx.closePath();
ctx.strokeStyle = lingrad;
ctx.stroke();
ctx.fillStyle = lingrad;
ctx.fill();
}
else {
ctx.fillStyle = '#000';
ctx.font = '24px Sans-Serif';
ctx.fillText('There is no data to display', (xOrig + xLen * 0.3), (yOrig - yLen * 0.5));
}
ctx.restore();
}
</script>
Here's what it looks like with some rendered data: Employee Sales Chart
Final note: I handled the collection of the data points and data labels using a static class object within my program, and passed this object to the HTML (Razor) page for my project. But, this solution should work if you have a way to get the data to your JS functions. If nothing else, you can start playing with this by defining the dataSet, xLabs, yLabs, range, and domain variables first and then figuring out how to pass larger data sets to the function later.

JavaScript Font Metrics

Given (1) a font-family and (2) a unicode character code.
Is it possible to, within JavaScript, produce an image that looks like:
http://www.freetype.org/freetype2/docs/tutorial/metrics.png
Basically, I want to:
display the character itself (enlarged)
get the various font metrics
draw a bunch of light grey lines
Now, drawing the light grey lines is simple -- I just use SVG. However, how do I extract the font-metrics of the character?
Based on the library mentioned above I made this codepen
http://codepen.io/sebilasse/pen/gPBQqm?editors=1010
[edit: capHeight is now based on the letter H as suggested by #sebdesign below]
HTML
<h4>
Change font name
<input value="Maven Pro"></input>
<small>[local or google]</small>
and font size
<input value="40px" size=8></input>
and
<button onclick="getMetrics()">
<strong>get metrics</strong>
</button>
</h4>
<div id="illustrationContainer"></div>
<pre id="log"></pre>
<canvas id="cvs" width="220" height="200"></canvas>
JS
(getMetrics());
function getMetrics() {
var testtext = "Sixty Handgloves ABC";
// if there is no getComputedStyle, this library won't work.
if(!document.defaultView.getComputedStyle) {
throw("ERROR: 'document.defaultView.getComputedStyle' not found. This library only works in browsers that can report computed CSS values.");
}
// store the old text metrics function on the Canvas2D prototype
CanvasRenderingContext2D.prototype.measureTextWidth = CanvasRenderingContext2D.prototype.measureText;
/**
* shortcut function for getting computed CSS values
*/
var getCSSValue = function(element, property) {
return document.defaultView.getComputedStyle(element,null).getPropertyValue(property);
};
// debug function
var show = function(canvas, ctx, xstart, w, h, metrics)
{
document.body.appendChild(canvas);
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
ctx.beginPath();
ctx.moveTo(xstart,0);
ctx.lineTo(xstart,h);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(xstart+metrics.bounds.maxx,0);
ctx.lineTo(xstart+metrics.bounds.maxx,h);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0,h/2-metrics.ascent);
ctx.lineTo(w,h/2-metrics.ascent);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0,h/2+metrics.descent);
ctx.lineTo(w,h/2+metrics.descent);
ctx.closePath();
ctx.stroke();
}
/**
* The new text metrics function
*/
CanvasRenderingContext2D.prototype.measureText = function(textstring) {
var metrics = this.measureTextWidth(textstring),
fontFamily = getCSSValue(this.canvas,"font-family"),
fontSize = getCSSValue(this.canvas,"font-size").replace("px",""),
isSpace = !(/\S/.test(textstring));
metrics.fontsize = fontSize;
// for text lead values, we meaure a multiline text container.
var leadDiv = document.createElement("div");
leadDiv.style.position = "absolute";
leadDiv.style.opacity = 0;
leadDiv.style.font = fontSize + "px " + fontFamily;
leadDiv.innerHTML = textstring + "<br/>" + textstring;
document.body.appendChild(leadDiv);
// make some initial guess at the text leading (using the standard TeX ratio)
metrics.leading = 1.2 * fontSize;
// then we try to get the real value from the browser
var leadDivHeight = getCSSValue(leadDiv,"height");
leadDivHeight = leadDivHeight.replace("px","");
if (leadDivHeight >= fontSize * 2) { metrics.leading = (leadDivHeight/2) | 0; }
document.body.removeChild(leadDiv);
// if we're not dealing with white space, we can compute metrics
if (!isSpace) {
// Have characters, so measure the text
var canvas = document.createElement("canvas");
var padding = 100;
canvas.width = metrics.width + padding;
canvas.height = 3*fontSize;
canvas.style.opacity = 1;
canvas.style.fontFamily = fontFamily;
canvas.style.fontSize = fontSize;
var ctx = canvas.getContext("2d");
ctx.font = fontSize + "px " + fontFamily;
var w = canvas.width,
h = canvas.height,
baseline = h/2;
// Set all canvas pixeldata values to 255, with all the content
// data being 0. This lets us scan for data[i] != 255.
ctx.fillStyle = "white";
ctx.fillRect(-1, -1, w+2, h+2);
ctx.fillStyle = "black";
ctx.fillText(textstring, padding/2, baseline);
var pixelData = ctx.getImageData(0, 0, w, h).data;
// canvas pixel data is w*4 by h*4, because R, G, B and A are separate,
// consecutive values in the array, rather than stored as 32 bit ints.
var i = 0,
w4 = w * 4,
len = pixelData.length;
// Finding the ascent uses a normal, forward scanline
while (++i < len && pixelData[i] === 255) {}
var ascent = (i/w4)|0;
// Finding the descent uses a reverse scanline
i = len - 1;
while (--i > 0 && pixelData[i] === 255) {}
var descent = (i/w4)|0;
// find the min-x coordinate
for(i = 0; i<len && pixelData[i] === 255; ) {
i += w4;
if(i>=len) { i = (i-len) + 4; }}
var minx = ((i%w4)/4) | 0;
// find the max-x coordinate
var step = 1;
for(i = len-3; i>=0 && pixelData[i] === 255; ) {
i -= w4;
if(i<0) { i = (len - 3) - (step++)*4; }}
var maxx = ((i%w4)/4) + 1 | 0;
// set font metrics
metrics.ascent = (baseline - ascent);
metrics.descent = (descent - baseline);
metrics.bounds = { minx: minx - (padding/2),
maxx: maxx - (padding/2),
miny: 0,
maxy: descent-ascent };
metrics.height = 1+(descent - ascent);
}
// if we ARE dealing with whitespace, most values will just be zero.
else {
// Only whitespace, so we can't measure the text
metrics.ascent = 0;
metrics.descent = 0;
metrics.bounds = { minx: 0,
maxx: metrics.width, // Best guess
miny: 0,
maxy: 0 };
metrics.height = 0;
}
return metrics;
};
//callback();
var fontName = document.getElementsByTagName('input')[0].value;
var fontSize = document.getElementsByTagName('input')[1].value;
var WebFontConfig = {
google: {
families: [ [encodeURIComponent(fontName),'::latin'].join('') ]
}
};
var wf = document.createElement('script');
wf.src = ('https:' == document.location.protocol ? 'https' : 'http') +
'://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js';
wf.type = 'text/javascript';
wf.async = 'true';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(wf, s);
document.body.style.fontFamily = ['"'+fontName+'"', "Arial sans"].join(' ')
var canvas = document.getElementById('cvs'),
context = canvas.getContext("2d");
var w=220, h=200;
canvas.style.font = [fontSize, fontName].join(' ');
context.font = [fontSize, fontName].join(' ');
context.clearRect(0, 0, canvas.width, canvas.height);
// draw bounding box and text
var xHeight = context.measureText("x").height;
var capHeight = context.measureText("H").height;
var metrics = context.measureText("Sxy");
var xStart = (w - metrics.width)/2;
context.fontFamily = fontName;
context.fillStyle = "#FFAF00";
context.fillRect(xStart, h/2-metrics.ascent, metrics.bounds.maxx-metrics.bounds.minx, 1+metrics.bounds.maxy-metrics.bounds.miny);
context.fillStyle = "#333333";
context.fillText(testtext, xStart, h/2);
metrics.fontsize = parseInt(metrics.fontsize);
metrics.offset = Math.ceil((metrics.leading - metrics.height) / 2);
metrics.width = JSON.parse(JSON.stringify(metrics.width));
metrics.capHeight = capHeight;
metrics.xHeight = xHeight - 1;
metrics.ascender = metrics.capHeight - metrics.xHeight;
metrics.descender = metrics.descent;
var myMetrics = {
px: JSON.parse(JSON.stringify(metrics)),
relative: {
fontsize: 1,
offset: (metrics.offset / metrics.fontsize),
height: (metrics.height / metrics.fontsize),
capHeight: (metrics.capHeight / metrics.fontsize),
ascender: (metrics.ascender / metrics.fontsize),
xHeight: (metrics.xHeight / metrics.fontsize),
descender: (metrics.descender / metrics.fontsize)
},
descriptions: {
ascent: 'distance above baseline',
descent: 'distance below baseline',
height: 'ascent + 1 for the baseline + descent',
leading: 'distance between consecutive baselines',
bounds: {
minx: 'can be negative',
miny: 'can also be negative',
maxx: 'not necessarily the same as metrics.width',
maxy: 'not necessarily the same as metrics.height'
},
capHeight: 'height of the letter H',
ascender: 'distance above the letter x',
xHeight: 'height of the letter x (1ex)',
descender: 'distance below the letter x'
}
}
Array.prototype.slice.call(
document.getElementsByTagName('canvas'), 0
).forEach(function(c, i){
if (i > 0) document.body.removeChild(c);
});
document.getElementById('illustrationContainer').innerHTML = [
'<div style="margin:0; padding:0; position: relative; font-size:',fontSize,'; line-height: 1em; outline:1px solid black;">',
testtext,
'<div class="__ascender" style="position: absolute; width:100%; top:',myMetrics.relative.offset,'em; height:',myMetrics.relative.ascender,'em; background:rgba(220,0,5,.5);"></div>',
'<div class="__xHeight" style="position: absolute; width:100%; top:',myMetrics.relative.offset + myMetrics.relative.ascender,'em; height:',myMetrics.relative.xHeight,'em; background:rgba(149,204,13,.5);"></div>',
'<div class="__xHeight" style="position: absolute; width:100%; top:',myMetrics.relative.offset + myMetrics.relative.ascender + myMetrics.relative.xHeight,'em; height:',myMetrics.relative.descender,'em; background:rgba(13,126,204,.5);"></div>',
'</div>'
].join('');
myMetrics.illustrationMarkup = document.getElementById('illustrationContainer').innerHTML;
var logstring = ["/* metrics for", fontName,
"*/\nvar metrics =",
JSON.stringify(myMetrics, null, ' ')].join(' ');
document.getElementById('log').textContent = logstring;
}

Drawing triangles on each piece of a canvas pie graph to make arrows

Using canvas, I have to take some data that is used to create a pie graph, and turn that pie graph into a circle of arrows.
So far the pie graph is simple, and cutting out the middle is easy. But can't figure out how to
get the x, y points of each end point of the pie peices.
use that to somehow draw a triangle, and
continue onto the next pie piece.
What I do have is the x, y of the center point of the circle, and the radius.
I can draw 1 triangle -
var friends=alumni=hopefuls=athletes=0,
data = ['Ahtlete', 'Athlete', 'Friend', 'Friend', 'Athlete'];
test = data.length;
for (var i = 0; i < test; i++){
if ( data[i] == "Athelete" ) {
athletes++;
} else if ( data[i] == "Friend"){
friends++;
} else if (data[i] == "Hopeful" ){
hopefuls++;
} else if (data[i] == "Alumni"){
alumni++
}
}
var mychart = new AwesomeChart('segmented-circle');
//friends, alumni, hopefuls, athletes;
mychart.colors = ['#d20007','#02f2fb', '#d2ebf6', '#0554ff']
mychart.title = "";
mychart.data = [friends, alumni, hopefuls, athletes];
mychart.labels = [];
mychart.chartType = 'ring';
mychart.draw();
function AwesomeChart(canvasElementId){
var canvas = document.getElementById(canvasElementId);
this.ctx = canvas.getContext('2d');
this.width = this.ctx.canvas.width;
this.height = this.ctx.canvas.height;
this.numberOfDecimals = 0;
this.proportionalSizes = true;
this.widthSizeFactor = this.width/400;
this.heightSizeFactor = this.height/400;
this.chartType = 'bar';
this.randomColors = false;
this.marginTop = 10;
this.marginBottom = 10;
this.marginLeft = 20;
this.marginRight = 50;
this.labelMargin = 10;
this.dataValueMargin = 20;
this.titleMargin = 10;
this.yAxisLabelMargin = 5;
this.data = new Array();
this.labels = new Array();
this.colors = new Array();
this.title = null;
this.backgroundFillStyle = 'rgba(255,255,255,0)';
this.borderStrokeStyle = 'rgba(255,255,255,0)';
this.borderWidth = 0;
this.labelFillStyle = 'rgb(220, 36, 0)';
this.labelFont = 'sans-serif';
this.labelFontHeight = 12;
this.labelFontStyle = '';
this.dataValueFillStyle = '#333';
this.dataValueFont = 'sans-serif';
this.dataValueFontHeight = 15;
this.dataValueFontStyle = '';
this.titleFillStyle = '#333';
this.titleFont = 'sans-serif';
this.titleFontHeight = 16;
this.titleFontStyle = 'bold';
this.yAxisLabelFillStyle = '#333';
this.yAxisLabelFont = 'sans-serif';
this.yAxisLabelFontHeight = 10;
this.yAxisLabelFontStyle = '';
var lingrad = this.ctx.createLinearGradient(0,0,0,this.height);
lingrad.addColorStop(0.2, '#fdfdfd');
lingrad.addColorStop(0.8, '#ededed');
this.chartBackgroundFillStyle = lingrad;
this.chartBorderStrokeStyle = '#999';
this.chartBorderLineWidth = 1;
this.chartHorizontalLineStrokeStyle = '#999';
this.chartHorizontalLineWidth = 1;
this.chartVerticalLineStrokeStyle = '#999';
this.chartVerticalLineWidth = 1;
this.chartMarkerSize = 5;
this.chartPointRadius = 4;
this.chartPointFillStyle = 'rgb(150, 36, 0)';
this.chartLineStrokeStyle = 'rgba(150, 36, 0, 0.5)';
this.chartLineWidth = 2;
this.barFillStyle = 'rgb(220, 36, 0)';
this.barStrokeStyle = '#fff';
this.barBorderWidth = 2.0;
this.barShadowColor = 'rgba(0, 0, 0, 0.5)';
this.barShadowBlur = 5;
this.barShadowOffsetX = 3.0;
this.barShadowOffsetY = 0.0;
this.barHGap = 20;
this.barVGap = 20;
this.explosionOffset = 20;
this.pieFillStyle = 'rgb(220, 36, 0)';
this.pieStrokeStyle = '#fff';
this.pieBorderWidth = 2.0;
this.pieShadowColor = 'rgba(255, 255, 255, 0.8)';
this.pieShadowBlur = 15;
this.pieShadowOffsetX = 0.0;
this.pieShadowOffsetY = 0.0;
this.pieStart = 0;
this.pieTotal = null;
this.generateRandomColor = function(){
var rgb = new Array();
for(var i=0; i<3; i++){
rgb.push(Math.ceil(Math.random()*150 + 50));
}
return 'rgb('+rgb.join(",")+')';
}
this.draw = function(){
var context = this.ctx;
context.lineCap = 'round';
var minFactor = Math.min(this.widthSizeFactor, this.heightSizeFactor);
if(this.proportionalSizes){
this.labelMargin = this.labelMargin * this.heightSizeFactor;
this.dataValueMargin = this.dataValueMargin * this.heightSizeFactor;
this.titleMargin = this.titleMargin * this.heightSizeFactor;
this.yAxisLabelMargin = this.yAxisLabelMargin * this.heightSizeFactor;
this.labelFontHeight = this.labelFontHeight * minFactor;
this.dataValueFontHeight = this.dataValueFontHeight * minFactor;
this.titleFontHeight = this.titleFontHeight * minFactor;
this.yAxisLabelFontHeight = this.yAxisLabelFontHeight * minFactor;
this.barHGap = this.barHGap * this.widthSizeFactor;
this.barVGap = this.barHGap * this.heightSizeFactor;
this.explosionOffset = this.explosionOffset * minFactor;
}
if(this.randomColors){
for(var i=0; i<this.data.length; i++){
if(!this.colors[i]){
this.colors[i] = this.generateRandomColor();
}
}
}
this.drawPieChart(true);
//Draw the outer border:
context.lineWidth = this.borderWidth;
context.strokeStyle = this.borderStrokeStyle;
context.strokeRect(0, 0, this.width, this.height);
context.globalCompositeOperation = 'destination-over';
//Fill the background:
context.fillStyle = this.backgroundFillStyle;
context.fillRect(0, 0, this.width, this.height);
context.globalCompositeOperation = 'source-over';
}
this.drawPieChart = function(ring){
var context = this.ctx;
context.lineWidth = this.pieBorderWidth;
var dataSum = 0;
if(this.pieTotal == null){
var len = this.data.length;
for (var i = 0; i < len; i++){
dataSum += this.data[i];
if(this.data[i]<0){
return;
}
}
}else{
dataSum = this.pieTotal;
}
var pieAreaWidth = this.width - this.marginLeft - this.marginRight;
var pieAreaHeight = this.height - this.marginTop - this.marginBottom;
var centerX = this.width / 2;
var centerY = this.marginTop + (pieAreaHeight / 2);
var doublePI = 2 * Math.PI;
var radius = (Math.min( pieAreaWidth, pieAreaHeight) / 2);
var maxLabelWidth = 0;
var labelWidth = 0;
context.font = this.labelFontStyle + ' ' + this.labelFontHeight + 'px '+ this.labelFont;
for (var i = 0; i < this.labels.length; i++){
labelWidth = context.measureText(this.labels[i]).width;
if(labelWidth > maxLabelWidth){
maxLabelWidth = labelWidth;
}
}
radius = radius - maxLabelWidth - this.labelMargin;
var startAngle = this.pieStart * doublePI / dataSum;
var currentAngle = startAngle;
var endAngle = 0;
var incAngleBy = 0;
for (var i = 0; i < this.data.length; i++){
context.beginPath();
incAngleBy = this.data[i] * doublePI / dataSum;
endAngle = currentAngle + incAngleBy;
// Draw pie graph then using same calculations
// draw an isoceles triangle at end points before the center
// is cut out to make the arrows
context.moveTo(centerX, centerY);
context.arc(centerX, centerY, radius, currentAngle, endAngle, false);
context.lineTo(centerX, centerY);
var twidth = 15; // Triangle Width
var theight = 20; // Triangle Height
context.moveTo(centerX+radius/2, centerY+radius/2);
context.lineTo(centerX+radius, centerY+radius);
context.lineTo(centerX+radius-10, centerY+radius/3);
context.lineTo(centerX, centerY);
/*
var width = 15; // Triangle Width
var height = 20; // Triangle Height
var padding = endAngle;
context.save();
context.moveTo(centerX, dataSum);
context.arc(centerX *2, dataSum, 5, 0, doublePI, false);
context.moveTo(padding + width / 2, padding); // Top Corner
context.lineTo(padding + width, height + padding); // Bottom Right
context.lineTo(centerX , height + padding); // Bottom Left
*/
currentAngle = endAngle;
if (this.colors[i]){
context.fillStyle = this.colors[i];
}else{
context.fillStyle = this.pieFillStyle;
}
context.fill();
context.strokeStyle = this.pieStrokeStyle;
context.stroke();
}
//Draw the outer shadow:
context.save();
context.shadowOffsetX = this.pieShadowOffsetX;
context.shadowOffsetY = this.pieShadowOffsetY;
context.translate(centerX, centerY);
//context.rotate(this.pieStart* doublePI / dataSum);
context.beginPath();
context.moveTo(0, 0);
context.arc(0, 0, radius, startAngle, endAngle, false);
context.shadowBlur = this.pieShadowBlur;
context.shadowColor = this.pieShadowColor;
context.globalCompositeOperation = 'destination-over';
context.fillStyle = 'rgba(0,0,0,1.0)';
context.fill();
context.restore();
//Ring-charts:
if(ring){
// "cut" the central part:
context.save();
var ringCenterRadius = radius / 1.15;
context.beginPath();
context.moveTo(centerX + ringCenterRadius, centerY);
context.arc(centerX, centerY, ringCenterRadius, 0, doublePI, false);
// context.globalCompositeOperation = 'destination-out';
context.fillStyle = '#000';
context.fill();
context.restore();
// draw the ring's inner shadow below the ring:
context.save();
context.shadowOffsetX = this.pieShadowOffsetX;
context.shadowOffsetY = this.pieShadowOffsetY;
context.translate(centerX, centerY);
context.beginPath();
// added -1 to make inner border same width as outer
context.arc(0, 0, ringCenterRadius -1, startAngle, endAngle, false);
context.shadowBlur = this.pieShadowBlur;
context.shadowColor = this.pieShadowColor;
context.globalCompositeOperation = 'destination-over';
context.strokeStyle = this.pieStrokeStyle;
context.stroke();
context.restore();
}
// draw the labels:
var currentAngle = this.pieStart* doublePI / dataSum;
var endAngle = 0;
var incAngleBy = 0;
context.beginPath();
for(var i=0; i<this.data.length; i++){
context.save();
incAngleBy = this.data[i] * doublePI / dataSum;
endAngle = currentAngle + incAngleBy;
var mAngle = currentAngle + incAngleBy/2;
context.translate(centerX, centerY);
context.rotate(mAngle);
context.font = this.labelFontStyle + ' ' + this.labelFontHeight + 'px '+ this.labelFont;
if(this.colors[i]){
context.fillStyle = this.colors[i];
}else{
context.fillStyle = this.labelFillStyle;
}
context.textAlign = 'start';
if(this.labels[i]){
if( (currentAngle>Math.PI/2) && (currentAngle<=3*(Math.PI/2)) ){
var translateXBy = radius + this.labelMargin + context.measureText(this.labels[i]).width / 2;
context.translate(translateXBy, 0);
context.rotate(Math.PI);
context.translate(-translateXBy, 0);
}
context.textBaseline = 'middle';
context.fillText(this.labels[i], radius+this.labelMargin, 0);
}
context.restore();
currentAngle = endAngle;
}
}
}
canvas {
background: #000033;
}
<canvas id='segmented-circle' width='200' height='200'></canvas>
I have tried several different things and feel like this would be a great thing to ask here.
The last one here is how it should look in the end.

Categories

Resources