Chart js - Draw center of each bubbles of a bubble chart - javascript

I have a bubble chart with big bubbles. I want to draw a cross or anything in the center of each bubble, but I can't find any solutions. The following picture show you the context:
I use Chart.js 2.5.0 on meteor.

Your question seemed quite interesting to me, therefore, I constructed the following chartjs plugin, that will help fulfill your requirement.
Chart.plugins.register({
afterDraw: c => {
let datasets = c.data.datasets;
datasets.forEach((e, i) => {
let isHidden = e._meta[0].hidden;
if (!isHidden) {
let data = c.getDatasetMeta(i).data;
data.forEach(e => {
let ctx = c.chart.ctx;
let x = e._model.x;
let y = e._model.y;
let r = e._model.radius;
// draw a cross
// or you can draw anything using general canvas methods
ctx.save();
ctx.beginPath();
ctx.moveTo(x - r / 4, y - r / 4);
ctx.lineTo(x + r / 4, y + r / 4);
ctx.moveTo(x + r / 4, y - r / 4);
ctx.lineTo(x - r / 4, y + r / 4);
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
});
}
});
}
});
ᴅᴇᴍᴏ
Chart.plugins.register({
afterDraw: c => {
let datasets = c.data.datasets;
datasets.forEach((e, i) => {
let isHidden = e._meta[0].hidden;
if (!isHidden) {
let data = c.getDatasetMeta(i).data;
data.forEach(e => {
let ctx = c.chart.ctx;
let x = e._model.x;
let y = e._model.y;
let r = e._model.radius;
// draw a cross
// or you can draw anything using general canvas methods
ctx.save();
ctx.beginPath();
ctx.moveTo(x - r / 4, y - r / 4);
ctx.lineTo(x + r / 4, y + r / 4);
ctx.moveTo(x + r / 4, y - r / 4);
ctx.lineTo(x - r / 4, y + r / 4);
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
});
}
});
}
});
let ctx = document.querySelector('#c').getContext('2d');
let chart = new Chart(ctx, {
type: 'bubble',
data: {
labels: ['Jan', 'Feb', 'Mar'],
datasets: [{
label: 'John',
data: [
{ x: 5, y: 5, r: 10 },
{ x: 10, y: 10, r: 15 },
{ x: 16, y: 15, r: 18 }
],
backgroundColor: '#76d1bf'
}, {
label: 'Smith',
data: [
{ x: 3, y: 10, r: 10 },
{ x: 7, y: 11, r: 15 },
{ x: 12, y: 6, r: 18 }
],
backgroundColor: '#827ada'
}]
},
options: {
responsive: false,
scales: {
xAxes: [{
ticks: {
min: 2,
max: 18,
stepSize: 4
}
}],
yAxes: [{
ticks: {
min: 0,
max: 20,
stepSize: 4
}
}]
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script>
<canvas id="c" height="200"></canvas>

Chart.js has always been a very straight forward and simple library. One of the advantages this has is that it is easy to customize. This is also one of the good points of JavaScript, source code is always available for what ever library you use.
There is how ever one draw back with customization. Once you have made changes to the library you will have to use that particular version because customization is not something that the authors will consider when making changes.
So to use the code below you should go to the github page and download the project and use that version of chart.js for your site. I dont change the original, most of the time each customization is specific for a particular case and the code is customised at the clients side.
The change is very simple. First backup the function that draws a point
Chart.canvasHelpers.defaultDrawPoint = Chart.canvasHelpers.drawPoint;
This is done so you can pass on all calls that you are not interested in back to the standard handler.
Next write the intercept code by replacing the function you just backed up.
Chart.canvasHelpers.drawPoint = function(ctx, pointStyle, radius, x, y){
Looking at the original source code you can work out how it draws the circles and just trap that behavior passing on all other argument variants to the original.
If pointStyle is undefined or === "circle" you handle that your self
// pointStyle undefined is default
// pointStyle === "circle" is the default named style
if(pointStyle === undefined || pointStyle === "circle"){
// next 4 lines copied from the source
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
You then add your custom code to render what ever you like. It is important that you save the current 2D context as you dont want to have to worry that you break something further down the line.
// draw a cross
ctx.save(); // save the state
ctx.strokeStyle = "white";
ctx.strokeWidth = 4;
ctx.beginPath();
ctx.moveTo(x - radius *0.3, y - radius *0.3);
ctx.lineTo(x + radius *0.3, y + radius *0.3);
ctx.moveTo(x + radius *0.3, y - radius *0.3);
ctx.lineTo(x - radius *0.3, y + radius *0.3);
ctx.stroke();
Then restore the state of the 2D context
ctx.restore(); // restore the state
The else handles the standard calls you are not interested in
}else{ // all other styles pass on to default handler
Chart.canvasHelpers.defaultDrawPoint(ctx, pointStyle, radius, x, y);
}
For Chart.js this is particularly useful as it gives a way to customize and get the animations as well.
Chart.canvasHelpers.defaultDrawPoint = Chart.canvasHelpers.drawPoint;
Chart.canvasHelpers.drawPoint = function(ctx, pointStyle, radius, x, y){
// PointStyle undefined is default
// PointStyle === "circle" is the default named style
if(pointStyle === undefined || pointStyle === "circle"){
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
// custom code here
ctx.save(); // save the state
ctx.strokeStyle = "white";
ctx.strokeWidth = 4;
ctx.beginPath();
ctx.moveTo(x - radius *0.3, y - radius *0.3);
ctx.lineTo(x + radius *0.3, y + radius *0.3);
ctx.moveTo(x + radius *0.3, y - radius *0.3);
ctx.lineTo(x - radius *0.3, y + radius *0.3);
ctx.stroke();
ctx.restore(); // restor the state
}else{ // all other styles pass on to default handler
Chart.canvasHelpers.defaultDrawPoint(ctx, pointStyle, radius, x, y);
}
}
// some utils to add data
// returns a random int
const rand = (min, max = min + (min = 0))=> Math.floor( Math.random() * (max-min) + min);
// returns a random data point {x,y,r}
const randData = ()=>({x : rand(0,50), y: rand(5,50), r: rand(4,20)});
// create a chart.
const ctx = canvas.getContext("2d");
const chart = new Chart(ctx, {
type: "bubble",
data: {
datasets: [{
label: "Random Data",
backgroundColor: "#7AF",
data: (()=>{
var dat = [];
for(var i = 0; i < 10; i++){ dat.push(randData()) }
return dat;
})(),
}]
},
options: { responsive: false } // this is required to work or it throws
// not sure why by it is not due to the
// changes
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script>
<canvas id=canvas height=200 width=400></canvas>
To roll back the changes simply set the function back to the original.
Chart.canvasHelpers.drawPoint = Chart.canvasHelpers.defualtDrawPoint;
And remove the extra reference
Chart.canvasHelpers.defualtDrawPoint = undefined;

grunt's answer did not work for me. I don't know if there are different formats or if it's written in an outdated version but I have modified his solution to the following so it would work in my project. Basically I have
removed the register since it wasn't in line with what the example for background-color was like and
found out that the data points' info isn't as nested anymore so I changed the way the properties are accessed, too
plugins: [
{
id: 'background-colour',
beforeDraw: (chart) => {
const ctx = chart.ctx;
ctx.save();
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, height);
ctx.restore();
}
},
{
id: 'abc',
afterDraw: (c) => {
let datasets = c.data.datasets;
datasets.forEach((e, i) => {
let isHidden = e.hidden;
if (!isHidden) {
let data = c.getDatasetMeta(i).data;
data.forEach(e => {
let ctx = c.ctx;
let x = e.x;
let y = e.y;
let r = e.options.radius as number;
ctx.save();
ctx.beginPath();
ctx.moveTo(x - r / 4, y - r / 4);
ctx.lineTo(x + r / 4, y + r / 4);
ctx.moveTo(x + r / 4, y - r / 4);
ctx.lineTo(x - r / 4, y + r / 4);
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
});
}
});
}
}
]

Related

How to centre align my line drawing using Canvas?

Problem
I am trying to put this line drawing in the center of my canvas, when I try to use the moveTo(100, 400) for the x-axis, it does not change the horizontal start position to 100. If I try the same thing with the y-axis it will move the the line along the x-axis.
I also need help with drawing the y-axis numbers 1 - 9 vertically along the y-axis it seems to only align horizontally.
EDIT!: I have manually stroked each point on the y-axis so I have the numbers on there, now I just want to know how to move the graph to center!!
Script
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.linecap = 'round';
// draw a scale with the numbers on it
ctx.lineWidth = 2;
ctx.strokeStyle = '#FF9900';
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.moveTo(100, 400);
for (i = 0; i <= 6; i+=1) {
//put a stroke mark
ctx.lineTo(100*i,400);
ctx.lineTo(100*i,405); //markers
ctx.lineTo(100*i,400);
// write the number 10px below
ctx.strokeStyle = '#000000';
// default size is 10px
ctx.strokeText(i, 100*i, 415);
ctx.strokeStyle = '#FF9900';
}
// draw a vertical scale with lines on it
ctx.moveTo(0, -100);
for (b = 0; b <= 9; b+=1) {
//put a stroke mark
ctx.lineTo(0,44.5*b);
ctx.lineTo(5,44.5*b);
ctx.lineTo(0,44.5*b);
// write the number 10px below
ctx.strokeStyle = '#000000';
// default size is 10px
}
ctx.strokeStyle = '#000000'
ctx.strokeText(1, 8, 365);
ctx.strokeText(2, 8, 320.5);
ctx.strokeText(3, 8, 276);
ctx.strokeText(4, 8, 231.5);
ctx.strokeText(5, 8, 187);
ctx.strokeText(6, 8, 142.5);
ctx.strokeText(7, 8, 98);
ctx.strokeText(8, 8, 53.5);
ctx.strokeText(9, 8, 9);
ctx.strokeStyle = '#FF9900';
ctx.stroke();
<!DOCTYPE html>
<html>
<head>
<title>Canvas Axis calibration</title>
<link rel="stylesheet" type="text/css" href="base.css"/>
</head>
<body>
<canvas id="myCanvas" width="1600" height="500"style="border:1px solid #c3c3c3;">
Canvas is not playing!
</canvas>
</body>
</html>
moveTo() just set starting point for your line, it's not draw actual line. Use lineTo() for draw actual line. so moveTo() is from or where you begin and lineTo() is where you go. So starting point for x axis must be moveTo(800, 0).
var c = document.getElementById("myCanvas"),
ctx = c.getContext("2d"),
lineWidth = 2,
xNumber = 6,
yNumber = 9,
xCenter = c.width / 2,
yCenter = 44.5 * yNumber + 44.5
ctx.linecap = 'round';
// draw a scale with the numbers on it
ctx.lineWidth = lineWidth;
ctx.strokeStyle = '#FF9900';
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.moveTo(xCenter, yCenter);
for (i = 0; i <= xNumber; ++i) {
//put a stroke mark
ctx.lineTo((xCenter + (100 * i)), yCenter);
ctx.lineTo((xCenter + (100 * i)), (yCenter + 5)); //markers
ctx.lineTo((xCenter + (100 * i)), yCenter);
// write the number 10px below
ctx.strokeStyle = '#000000';
// default size is 10px
ctx.strokeText(i, (xCenter + (100 * i)), (yCenter + 15));
}
ctx.strokeStyle = '#FF9900';
ctx.stroke()
// draw a vertical scale with lines on it
ctx.beginPath()
ctx.moveTo(xCenter, yCenter);
for (b = 0; b <= yNumber; ++b) {
//put a stroke mark
if(b === 0) continue;
ctx.lineTo(xCenter, (yCenter - (44.5 * b)));
ctx.lineTo((xCenter - 5), (yCenter - (44.5 * b)));
ctx.lineTo(xCenter, (yCenter - (44.5 * b)));
ctx.strokeStyle = '#000000';
ctx.strokeText(b, (xCenter - 15), (yCenter - (44.5 * b)));
}
ctx.strokeStyle = '#FF9900';
ctx.stroke();
<!DOCTYPE html>
<html>
<head>
<title>Canvas Axis calibration</title>
<link rel="stylesheet" type="text/css" href="base.css"/>
</head>
<body>
<canvas id="myCanvas" width="1600" height="500"style="border:1px solid #c3c3c3;">
Canvas is not playing!
</canvas>
</body>
</html>
CanvasRenderingContext2D has a method for that: translate(). It simply sets a coordinate-shift which is going to be applied to everything you draw afterwards:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.linecap = 'round';
ctx.lineWidth = 2;
ctx.fillStyle = 'blue';
ctx.translate((1600-500)/2,0); // <-----------
ctx.strokeStyle = '#000000';
ctx.beginPath();
ctx.moveTo(100, 400);
for (var i = 0; i <= 6; i+=1) {
ctx.lineTo(100*i,400);
ctx.lineTo(100*i,405);
ctx.lineTo(100*i,400);
ctx.strokeText(i, 100*i, 415);
}
ctx.moveTo(0, -100);
for (var b = 0; b <= 9; b+=1) {
ctx.lineTo(0,44.5*b);
ctx.lineTo(5,44.5*b);
ctx.lineTo(0,44.5*b);
if(b<9)
ctx.strokeText(b+1, 8, 365-44.5*b);
}
ctx.strokeStyle = '#FF9900';
ctx.stroke();
<!DOCTYPE html>
<html>
<head>
<title>Canvas Axis calibration</title>
<link rel="stylesheet" type="text/css" href="base.css"/>
</head>
<body>
<canvas id="myCanvas" width="1600" height="500"style="border:1px solid #c3c3c3;">Canvas is not playing!</canvas>
</body>
</html>
Here I assumed the drawing is 500 units wide, which does not seem to be entirely correct, but you will certainly see the result of translate(). The effect of translate() can be reset with setTransform(1, 0, 0, 1, 0, 0) call (if you are familiar with homogeneous coordinates and transformation matrices, note it has a heavily modified order, see in docs). It is actually a matrix which can do all kinds of 2D transformations (translation, rotation, scaling, skewing), translate() is just a convenience function (the equivalent call probaby would be setTransform(1,0,0,1,(1600-500)/2,0), but I have not tried).
Minor changes:
added the var-s into the loops: otherwise variables become global ones which is usually not a problem for a loop variable like i, but generally considered bad practice
reduced to a single stroke() and two strokeStyle-s. The thing is that lines, arcs and the like are drawn with the settings which are set at the very moment when you call stroke(), it does not matter what happened in between. So color is black for most of the time, as strokeText() is immediate, and color becomes that beige/whatever one just for the stroke()
moved the second set of labels into the corresponding loop. I am not sure if the loop is entirely correct, as 9 labels and 9 line segments are visible, but 10 line segments are drawn.
Think local origins
When you get a building plan you don't get a giant sheet of paper with the plan way in the corner because you are building in the burbs, you want to move some windows out of the summer sun, you don't redraw the plan with new coordinates for each wall.
No you get the plan that fits a small sheet, on the plan is a location and orientation. The position of the walls are fix to the local coordinates of the plan.
Same goes for drawing in 2D. You can define a box as 4 points around an origin. [[-10,-10],[10,-10],[10,10],[-10,10]] and when you draw it you set its location and orientation, you dont change the position of each point to the new location.
Draw local coordinate in the world via setTransform
In the 2D API the position and orientation is set via a transform.
function drawPath(x,y, points) { // only position changes
ctx.setTransform(1,0,0,1,x,y); // set the location
ctx.beginPath();
for(const [x,y] of points) {
ctx.lineTo(x,y);
}
ctx.stroke();
}
const box = [[-10,-10],[10,-10],[10,10],[-10,10]];
drawPath(100, 100, box);
And with scale and rotate
function drawPath(x,y,scale, rotate, points) {
const xdx = Math.cos(rotate) * scale;
const xdy = Math.sin(rotate) * scale;
ctx.setTransform(xdx, xdy, -xdy, xdx, x, y); // set the location
ctx.beginPath();
for(const [x,y] of points) {
ctx.lineTo(x,y);
}
ctx.stroke();
}
drawPath(100, 100, 2, 0.5, box);
const box = [[-10,-10],[10,-10],[10,10],[-10,10]];
const W = canvas.width;
const H = canvas.height;
const ctx = canvas.getContext("2d");
ctx.font = "2opx arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "red";
const rand = v => Math.random() * v;
drawRandom();
function drawPath(x, y,scale, rotate, points) {
const xdx = Math.cos(rotate) * scale;
const xdy = Math.sin(rotate) * scale;
ctx.setTransform(xdx, xdy, -xdy, xdx, x, y); // set the location
ctx.fillText("Hi",0,0);
ctx.beginPath();
for(const [x,y] of points) {
ctx.lineTo(x, y);
}
ctx.closePath();
ctx.setTransform(1, 0, 0, 1, 0, 0); // Resets so line width remains 1 px
ctx.stroke();
}
function drawRandom() {
drawPath(rand(W), rand(H), rand(2) + 0.5, rand(Math.PI * 2), box);
setTimeout(drawRandom, 500);
}
canvas {
border: 1px solid black;
}
<canvas id="canvas" width ="400" height="400"></canvas>
All you need is ctx.setTransform and maybe ctx.transform if you are doing rigged animation stuff. I never use ctx.translate, ctx.scale, ctx.rotate because they are slow, and its hard to picture just where you are, oh and did I say they are SLOW!!!!
To reset the transform (remove scale, rotation and move back to 0,0) call ctx.resetTransform() or ctx.setTransform(1,0,0,1,0,0)
And some more regarding your approach to the code.
Granular coding
Looks like you want to draw a graph.
Manually drawing every tick, setting styles, and dozens of magic numbers and values is not going to make it much fun. Worse is that when it comes time to make changes it will take forever.
Don't repeat
You need to think like a lazy programmer. Create functions so you dont have to do the same thing over and over.
Define styles once and name them
For example setting the 2D context style is a pain. A drawing usually only has a few different styles, so create an object with named styles
const styles = {
textHang: {
textAlign : "center",
textBaseline : "top",
fillStyle: "blue",
font: "16px Arial",
},
};
And a function that will set a style
const setStyle = (style, c = ctx) => Object.assign(c, style);
Now you can set a style
const ctx = myCanvas.getContext("2d");
setStyle(styles, styles.textHang);
ctx.fillText("The text", 100, 100);
Basic 2D point helper
You are working in 2D and 2D uses a lot of points. You will be adding multiplying, copying... 2D points over and over and over.
Reduce the typing and cover the most basic 2D needs with only 7 functions
const P2 = (x = 0, y = 0) => ({x,y});
const P2Set = (p, pAs) => (p.x = pAs.x, p.y = pAs.y, p);
const P2Copy = p => P2(p.x, p.y);
const P2Mult = (p, val) => (p.x *= val, p.y *= val, p);
const P2Add = (p, pAdd) => (p.x += pAdd.x, p.y += pAdd.y, p);
const P2Sub = (p, pSub) => (p.x -= pSub.x, p.y -= pSub.y, p);
const P2Dist = (p1, p2) => ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** 0.5;
No line? 2D API
The 2D API is great, but lacking. To just draw a line is crazy long, foo bar....
ctx.linecap = 'round';
ctx.lineWidth = 2;
ctx.strokeStyle = '#FF9900';
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(410, 410);
ctx.stroke();
No way create functions, use named styles, don't enter coordinates use points.
Some common 2D tasks as functions
const clear = (c = ctx) => (setPos(), c.clearRect(0,0,c.canvas.width,c.canvas.height));
const line = (p1, p2, c = ctx) => (c.moveTo(p1.x, p1.y), c.lineTo(p2.x, p2.y))
const setPos = (p, c = ctx) => p ? c.setTransform(1, 0, 0, 1, p.x, p.y) : c.resetTransform();
const path = (p, path, c = ctx) => {
c.setTransform(1,0,0,1,p.x,p.y);
for(const seg of path) { // each segment
let first = true;
for(const p of seg) { // each point
first ? (c.moveTo(p.x,p.y), first = false):(c.lineTo(p.x, p.y));
}
}
}
Example
The following takes all the above and creates 2 Axis. It may seem like a lot extra, but as you add complexity to your drawing you quickly find you need less and less code.
/* Set up the context get common values eg W,H for width and height */
const W = canvas.width;
const H = canvas.height;
const ctx = canvas.getContext("2d");
// Helper functions will use a global ctx, or pass a 2d context as last argument
// P2 is a point. I use p to mean a point
const P2 = (x = 0, y = 0) => ({x,y});
const P2Set = (p, pAs) => (p.x = pAs.x, p.y = pAs.y, p);
const P2Copy = p => P2(p.x, p.y);
const P2Mult = (p, val) => (p.x *= val, p.y *= val, p);
const P2Add = (p, pAdd) => (p.x += pAdd.x, p.y += pAdd.y, p);
const P2Sub = (p, pSub) => (p.x -= pSub.x, p.y -= pSub.y, p);
const P2Dist = (p1, p2) => ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** 0.5;
const setStyle = (style, c = ctx) => Object.assign(c, style);
const clear = (c = ctx) => (setPos(0, c), c.clearRect(0,0,c.canvas.width,c.canvas.height));
const line = (p1, p2, c = ctx) => (c.moveTo(p1.x, p1.y), c.lineTo(p2.x, p2.y))
const setPos = (p, c = ctx) => p ? c.setTransform(1, 0, 0, 1, p.x, p.y) : c.resetTransform();
const path = (p, path, c = ctx) => {
setPos(p,c);
for(const seg of path) { // each segment
let first = true;
for(const p of seg) { // each point
first ? (c.moveTo(p.x,p.y), first = false):(c.lineTo(p.x, p.y));
}
}
}
const styles = { // define any of the 2D context properties you wish to set
textHang: {textAlign : "center", textBaseline : "top"},
textLeft: {textAlign : "left", textBaseline : "middle"},
markTextStyle: {fillStyle: "blue", font: "16px Arial"},
markStyle: {
strokeStyle: "black",
lineCap: "round",
lineWidth: 2,
},
};
const paths = { // Array of arrays of points. each sub array is a line segment
markLeft: [[P2(-2, 0), P2(5, 0)]],
markUp: [[P2(0, 2), P2(0, -5)]],
}
// Draw an axis from point to point, using mark to mark, lineStyle for the line
// marks is an array of names for each mark, markStyle is the style for the text marks
// markDist is the distance out (90 to the right) to put the text marks
function drawAxis(fromP, toP, mark, lineStyle, marks, markStyle, markDist) {
const norm = P2Mult(P2Sub(P2Copy(toP), fromP), 1 / P2Dist(fromP, toP));
const step = P2Mult(P2Sub(P2Copy(toP), fromP), 1 / (marks.length-1));
const pos = P2Copy(fromP);
setStyle(lineStyle);
ctx.beginPath();
setPos(); // without argument pos is 0,0
line(fromP, toP);
for(const m of marks) {
path(pos, mark);
P2Add(pos, step);
}
ctx.stroke();
P2Set(pos, fromP);
setStyle(markStyle);
for(const m of marks) {
setPos(pos);
ctx.fillText(m,-norm.y * markDist, norm.x * markDist)
P2Add(pos, step)
}
}
const insetW = W * 0.1;
const insetH = H * 0.1;
const axisText = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
clear();
drawAxis(
P2(insetW, H - insetH), P2(insetW, insetH), paths.markLeft,
styles.markStyle,
axisText,
{...styles.textLeft, ...styles.markTextStyle},
-18
);
drawAxis(
P2(insetW, H - insetH), P2(W - insetW, H - insetH), paths.markUp,
styles.markStyle,
axisText,
{...styles.textHang, ...styles.markTextStyle},
6
);
canvas {
border: 1px solid black;
}
<canvas id="canvas" width ="400" height="400"></canvas>

How to progressively draw any line using Canvas

I'm trying to draw lines progressively (currently using recursive functions) in a canvas element, and I'm able to successfully draw pairs of lines that are parallel to the x or y axes, this way:
function line(xa, ya, xb, yb) {
context.beginPath();
context.moveTo(xa, ya);
context.lineTo(xb, yb);
context.stroke();
}
(function drawLine(i){
line(155, i, 155, i-2);
line(245, i, 245, i-2);
if (i > 35) {
setTimeout(function(){
drawLine(i-2);
}, 10);
}
})(57);
And I get this:
(context.lineWidth is set to 10 and context.lineCap is set to round)
However, I've tried several ways of drawing pairs of lines that aren't strictly horizontal nor vertical but I'm only able to get something like this:
(function drawLine(i){
line(155, i, 155+(57-i)/2, i-2);
line(245, i, 245-(57-i)/2, i-2);
if (i > 35) {
setTimeout(function(){
drawLine(i-2);
}, 10);
}
})(57);
(changing the value of context.lineWidth or context.lineCap doesn't solve the problem)
Is there a way to draw any kind of line progressively in a canvas element?
In your first version, you draw lines from a point based on the current value of i to a point based on the value of i in the next iteration. But in the second version, the x value of your start point is a constant. Base the start point on i, and the end point on i - 2:
let c = document.querySelector('canvas');
let context = c.getContext('2d');
context.lineWidth = 10;
context.lineCap = 'round';
function line(xa, ya, xb, yb) {
context.beginPath();
context.moveTo(xa, ya);
context.lineTo(xb, yb);
context.stroke();
}
(function drawLine(i){
line(155 + (57 - i) / 2, i, 155 + (57 - (i - 2)) / 2, (i - 2));
line(245 - (57 - i) / 2, i, 245 - (57 - (i - 2)) / 2, (i - 2));
if (i > 35) {
setTimeout(function(){
drawLine(i-2);
}, 10);
}
})(57);
<canvas></canvas>
The easiest way is to use Canvas.js:
const canvas = new Canvas('my-canvas', 200, 200).start();
const line1 = new Canvas.Line({
from: {
x: 50,
y: 70
},
to: {
x: 60,
y: 30
},
lineWidth: 7,
lineCap: 'round',
lineLength: 0.1
});
canvas.addElement(line1);
line1.animate('lineLength', {lineLength: 1, duration: 500});
const line2 = new Canvas.Line({
from: {
x: 90,
y: 70
},
to: {
x: 80,
y: 30
},
lineWidth: 7,
lineCap: 'round',
lineLength: 0.1
});
canvas.addElement(line2);
line2.animate('lineLength', {lineLength: 1, duration: 500});
<script src="https://gustavgenberg.github.io/handy-front-end/Canvas.js"></script>
Using line dash to animate along paths.
A simple way to animate along any path is to use the line dash and line dash offset.
const ctx = canvas.getContext('2d');
ctx.lineWidth = 10;
ctx.lineCap = 'round';
function drawLines(){
function drawLine(x1, y1, x2, y2){
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
}
drawLine(10,10,490,90);
drawLine(10,190,490,110);
}
var lineLength = 30; // pixels
var distanceBetween = 400;
var lineSpeed = 300; //pixels per second
ctx.setLineDash([lineLength, distanceBetween]);
function animateLines(time){
ctx.lineDashOffset = -time * lineSpeed / 1000;
ctx.stroke();
}
function loop(time){
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
ctx.beginPath();
drawLines();
animateLines(time);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
<canvas id="canvas" width=500 height=200></canvas>

Performance-wise - canvas vs base URI vs image

I'm creating a color wheel (picker) and I want to know the fastest most efficient way to display the color wheel. I'm currently using JavaScript to generate it with a canvas. I think the other options are an actual image or data URI. If there is a faster way please let me know.
What's the fastest most efficient way to show the color picker?
Color Wheel using JavaScript / canvas
JSFiddle
var colorDisc = document.getElementById('surface'),
colorDiscRadius = colorDisc.offsetHeight / 2;
var drawDisk = function(ctx, coords, radius, steps, colorCallback) {
var x = coords[0] || coords, // coordinate on x-axis
y = coords[1] || coords, // coordinate on y-axis
a = radius[0] || radius, // radius on x-axis
b = radius[1] || radius, // radius on y-axis
angle = 360,
rotate = 0,
coef = Math.PI / 180;
ctx.save();
ctx.translate(x - a, y - b);
ctx.scale(a, b);
steps = (angle / steps) || 360;
for (; angle > 0; angle -= steps) {
ctx.beginPath();
if (steps !== 360) ctx.moveTo(1, 1); // stroke
ctx.arc(1, 1, 1, (angle - (steps / 2) - 1) * coef, (angle + (steps / 2) + 1) * coef);
if (colorCallback) {
colorCallback(ctx, angle);
} else {
ctx.fillStyle = 'black';
ctx.fill();
}
}
ctx.restore();
},
drawCircle = function(ctx, coords, radius, color, width) { // uses drawDisk
width = width || 1;
radius = [
(radius[0] || radius) - width / 2, (radius[1] || radius) - width / 2
];
drawDisk(ctx, coords, radius, 1, function(ctx, angle) {
ctx.restore();
ctx.lineWidth = width;
ctx.strokeStyle = color || '#000';
ctx.stroke();
});
};
if (colorDisc.getContext) {
drawDisk( // HSV color wheel with white center
colorDisc.getContext("2d"), [colorDisc.width / 2, colorDisc.height / 2], [colorDisc.width / 2 - 1, colorDisc.height / 2 - 1],
360,
function(ctx, angle) {
var gradient = ctx.createRadialGradient(1, 1, 1, 1, 1, 0);
gradient.addColorStop(0, 'hsl(' + (360 - angle + 0) + ', 100%, 50%)');
gradient.addColorStop(1, "#FFFFFF");
ctx.fillStyle = gradient;
ctx.fill();
}
);
drawCircle( // gray border
colorDisc.getContext("2d"), [colorDisc.width / 2, colorDisc.height / 2], [colorDisc.width / 2, colorDisc.height / 2],
'#555',
3
);
}
<canvas id="surface" width="500" height="500"></canvas>
I think an image would be faster, but it would be difficult to resize it without getting all kinds of scaling artifacts. So I would go with canvas.
However, there is a third option that I think is worth considering: angular gradient in CSS. Here is a way to do it with existing features: https://css-tricks.com/conical-gradients-css/

Canvas/JavaScript: How to adjust position of slices in donutgraph using canvas?

I am trying to draw a donut pie chart using Canvas. Which is almost done but facing some issue in adjusting position of slices.
Current:
Expected:
enter code here
http://jsfiddle.net/RgLAU/1/
I want 1.) yellow/blue to draw from top 2.) want to write some text inside the donut.
Here is my work upto now:
http://jsfiddle.net/RgLAU/1/
arc() method starts from an horizontal line, on the right of your shape, at middle y position of the shape's height.
You will need to add an offset to each of your start and end angle value.
For your text, I'm not sure what it should display, but setting the context's textAlign = "center" and textBaseline = "middle" will make it easy to position anywhere.
A rough uncleaned dump of your modified code :
var canvas = document.getElementById("chart");
var chart = canvas.getContext("2d");
function drawdountChart(canvas) {
// text options
chart.textAlign = "center";
chart.textBaseline = "middle";
chart.font = "25px sans-serif";
// where is our arc start angle
var offset = 1.5 * Math.PI;
this.x, this.y, this.radius, this.lineWidth, this.strockStyle, this.from, this.to = null;
this.set = function(x, y, radius, from, to, lineWidth, strockStyle) {
this.x = x;
this.y = y;
this.radius = radius;
this.from = from;
this.to = to;
this.lineWidth = lineWidth;
this.strockStyle = strockStyle;
}
this.draw = function(data) {
canvas.beginPath();
canvas.lineWidth = this.lineWidth;
canvas.strokeStyle = this.strockStyle;
canvas.arc(this.x, this.y, this.radius, this.from + offset, this.to + offset);
canvas.stroke();
var numberOfParts = data.numberOfParts;
var parts = data.parts.pt;
var colors = data.colors.cs;
var df = 0;
for (var i = 0; i < numberOfParts; i++) {
canvas.beginPath();
canvas.strokeStyle = colors[i];
canvas.arc(this.x, this.y, this.radius, df + offset, df + (Math.PI * 2) * (parts[i] / 100) + offset);
canvas.stroke();
df += (Math.PI * 2) * (parts[i] / 100);
}
chart.fillStyle = 'white'
chart.fillText('hello', this.x, this.y);
}
}
var data = {
numberOfParts: 4,
parts: {
"pt": [20, 30, 25, 25]
}, //percentage of each parts
colors: {
"cs": ["red", "green", "blue", "yellow"]
} //color of each part
};
var drawDount = new drawdountChart(chart);
drawDount.set(150, 150, 100, 0, Math.PI * 2, 30, "#fff");
drawDount.draw(data);
<canvas id="chart" width="500" height="500" style="background-color:black"> </canvas>

HTML5 Canvas pie chart

I'm attempting to create a simple pie chart like shown in the graphic below:
The chart will show the results for a quiz where a user can choose either a, b or c. They're 10 questions and the user can only choose one option per question.
What I want to do is show the pie chart with each segment being a percentage of 100% by passing in the values for either a,b, or c.
I have the following so far:
var greenOne = "#95B524";
var greenTwo = "#AFCC4C";
var greenThree = "#C1DD54";
function CreatePieChart() {
var chart = document.getElementById('piechart');
var canvas = chart.getContext('2d');
canvas.clearRect(0, 0, chart.width, chart.height);
var total = 100;
var a = 3;
var b = 4;
var c = 3;
for (var i = 0; i < 3; i++) {
canvas.fillStyle = "#95B524";
canvas.beginPath();
canvas.strokeStyle = "#fff";
canvas.lineWidth = 3;
canvas.arc(100, 100, 100, 0, Math.PI * 2, true);
canvas.closePath();
canvas.stroke();
canvas.fill();
}
}
CreatePieChart();
<canvas id="piechart" width="200" height="200"></canvas>
The colors are specific to the size of the segment, so green one is used for the largest and green three for the smallest.
Even after searching Google and triple-checking my radians values, etc. I was still having trouble with this, so I have created a jsFiddle for people to play with as a live example and will post the code below as well. (Update: in the fiddle v2, stroke and labels are added also.)
var canvas = document.getElementById("can");
var ctx = canvas.getContext("2d");
var lastend = 0;
var data = [200, 60, 15]; // If you add more data values make sure you add more colors
var myTotal = 0; // Automatically calculated so don't touch
var myColor = ["red", "green", "blue"]; // Colors of each slice
for (var e = 0; e < data.length; e++) {
myTotal += data[e];
}
for (var i = 0; i < data.length; i++) {
ctx.fillStyle = myColor[i];
ctx.beginPath();
ctx.moveTo(canvas.width / 2, canvas.height / 2);
ctx.arc(
canvas.width / 2, // x
canvas.height / 2, // y
canvas.height / 2, // radius
lastend, // startingAngle (radians)
lastend + Math.PI * 2 * (data[i] / myTotal), // endingAngle (radians)
false // antiClockwise (boolean)
);
ctx.lineTo(canvas.width / 2, canvas.height / 2);
ctx.fill();
lastend += Math.PI * 2 * (data[i] / myTotal);
}
<canvas id="can" width="200" height="200" />
I like the previous answer, but I felt it was lacking in code clarity and it didn't really cover how to utilize labels.
I moved the values into a data object array for easy declaration. Other values, like percentage, I explicitly declared as a property on the data object, or as a separate variable. This, I think, makes it easier to read.
The refactoring also made it easier to tie the values to input boxes if that's something you're interested in.
To see what I mean and play with the values check out this CodePen: http://codepen.io/zfrisch/pen/pRbZeb
var data = [
{
label: "one",
value: 100,
color: "white",
},
{
label: "two",
value: 100,
color: "skyBlue",
},
{
label: "three",
value: 100,
color: "yellow",
},
];
var total = 0;
for (obj of data) {
total += obj.value;
}
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var previousRadian;
var middle = {
x: canvas.width / 2,
y: canvas.height / 2,
radius: canvas.height / 2,
};
//background
ctx.beginPath();
ctx.arc(middle.x, middle.y, middle.radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
ctx.fillStyle = "black";
ctx.fill();
//end of background
for (obj of data) {
previousRadian = previousRadian || 0;
obj.percentage = parseInt((obj.value / total) * 100);
ctx.beginPath();
ctx.fillStyle = obj.color;
obj.radian = Math.PI * 2 * (obj.value / total);
ctx.moveTo(middle.x, middle.y);
//middle.radius - 2 is to add border between the background and the pie chart
ctx.arc(
middle.x,
middle.y,
middle.radius - 2,
previousRadian,
previousRadian + obj.radian,
false
);
ctx.closePath();
ctx.fill();
ctx.save();
ctx.translate(middle.x, middle.y);
ctx.fillStyle = "black";
ctx.font = middle.radius / 10 + "px Arial";
ctx.rotate(previousRadian + obj.radian);
var labelText = "'" + obj.label + "' " + obj.percentage + "%";
ctx.fillText(labelText, ctx.measureText(labelText).width / 2, 0);
ctx.restore();
previousRadian += obj.radian;
}
<canvas id="myCanvas" width="500" height="500"></canvas>
Here is a pie chart without using external libraries, using html5 canvas :
See the code
But it's better to use libraries for drawing charts. in apex-charts there is an option called sparkline, which helps you to remove the extra stuffs and draw a minimal and clean chart.
Here is a clean donut chart using apex-charts library. (Extra stuffs are removed with sparkline option):
var options = {
series: [620, 40],
labels: ['Finished', 'Unfinished'],
chart: {
type: 'donut',
sparkline: {
enabled: true,
}
},
plotOptions: {
pie: {
donut: {
labels: {
show: true,
total: {
showAlways: false,
show: true,
label: 'Total'
}
}
}
}
},
};
var chart = new ApexCharts(document.querySelector("#chart"), options);
chart.render();
See it on codepen
I had the same problem before but I was able to solve this problem later.
What I was missing was I was drawing an arch in context and was trying to fill it, due to which the color was spreading all across the circle because now the context was bound only between a radius line from center to the starting point of arch and the arch to bound the context.
But there was no other boundary the line from the end of arch to the center, as soon as I draw that line using the following:
ctx.lineTo(center coordinates of circle);
I have a complete boundary of a pie, so now if I fill the color in context it will not get spread inside the whole circle but will be limited to that pie.

Categories

Resources