Ive made a working circular graph using javascript, and Ive run into a problem.
The script fetches the id of the graph in order to work, but I would like multiple graphs.
So here is my question: How do I make the script compatible with multiple graphs? I tried fetching them by their class but that didnt seem to work.
Here is my code:
var el = document.getElementById('graph'); // get canvas
var options = {
percent: el.getAttribute('data-percent') || 25,
size: el.getAttribute('data-size') || 80,
lineWidth: el.getAttribute('data-line') || 5,
color: el.getAttribute('data-color') || 0,
rotate: el.getAttribute('data-rotate') || 0
}
var canvas = document.createElement('canvas');
var span = document.createElement('span');
span.textContent = options.percent + '%';
if (typeof(G_vmlCanvasManager) !== 'undefined') {
G_vmlCanvasManager.initElement(canvas);
}
var ctx = canvas.getContext('2d');
canvas.width = canvas.height = options.size;
el.appendChild(span);
el.appendChild(canvas);
ctx.translate(options.size / 2, options.size / 2); // change center
ctx.rotate((-1 / 2 + options.rotate / 180) * Math.PI); // rotate -90 deg
var radius = (options.size - options.lineWidth) / 2;
var drawCircle = function(color, lineWidth, percent) {
percent = Math.min(Math.max(0, percent || 1), 1);
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2 * percent, false);
ctx.strokeStyle = color;
ctx.lineCap = 'butt'; // butt, round or square
ctx.lineWidth = lineWidth
ctx.stroke();
};
drawCircle('#F7F7F7', options.lineWidth, 100 / 100);
drawCircle(options.color, options.lineWidth, options.percent / 100);
And this is the HTML:
<div class="chart" id="graph"
data-percent="92"
data-color="#1EA1FF"
data-size="110" data-line="3">
</div>
Thanks in advance.
You should be able to surround everything with a loop. You need to select all the elements with the same class into an array, then loop through that array. I also suggest setting the "el" variable inside the array, then you don't have to change your code:
var graphs = document.getElementsByClassName("chart");
for(var i = 0; i < graphs.length; i++)
{
var el = graphs[i];
//Now the rest of your code
var options = ...
}
"el" then becomes each element as the loop iterates.
Related
I have a need to add a bunch of dots within the confines of an ellipse. I'm trying to modify the code from the accepted solution to this question, which is set up to use a circle (same width and height) rather than an ellipse. It is also implementing via a HTML <canvas> element, and I need to get it working using p5.js.
Here is my code for the <canvas> element. It seems to be working great. i can size and position the ellipse any which way and the dots are all displayed within it no matter what.
function ellipse(context, cx, cy, rx, ry){
context.save(); // save state
context.beginPath();
context.translate(cx-rx, cy-ry);
context.scale(rx, ry);
context.arc(1, 1, 1, 0, 2 * Math.PI, false);
context.restore(); // restore to original state
context.stroke();
}
var canvas = document.getElementById("thecanvas");
var ctx = canvas.getContext('2d'),
count = 1000, // number of random points
cx = 300,
cy = 200,
w = 50,
h = 49,
radius = w;
ctx.fillStyle = '#CCCCCC';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ellipse(ctx, cx, cy, w, h);
// create random points
ctx.fillStyle = '#ffffff';
function dots() {
while (count) {
var pt_angle = Math.random() * 2 * Math.PI;
var pt_radius_sq = Math.random() * radius * radius;
var pt_x = Math.sqrt(pt_radius_sq) * Math.cos(pt_angle);
var pt_y = Math.sqrt(pt_radius_sq) * Math.sin(pt_angle);
ctx.fillRect( ((Math.sqrt(pt_radius_sq) * Math.cos(pt_angle) ) + cx), (((Math.sqrt(pt_radius_sq) * Math.sin(pt_angle))/(w/h)) + cy), 2, 2);
count--;
}
}
dots();
<canvas id="thecanvas" width="800" height="800"></canvas>
For some reason, what I believe to be a pretty straight forward to port to p5.js is not working. The dots seem to be contained within the same shaped ellipse as what is defined, but the scale seems to be off and I can't really tell what is causing it. You can see this in action at https://editor.p5js.org/dpassudetti/sketches/YRbLuwoM2 - p5.js code pasted below as well:
var count = 1000, // number of random points
cx = 300,
cy = 200,
w = 50,
h = 49,
radius = w;
function setup() {
createCanvas(800, 800);
function dots() {
fill('green');
stroke('green');
while (count) {
var pt_angle = Math.random() * 2 * Math.PI;
var pt_radius_sq = Math.random() * radius * radius;
var pt_x = Math.sqrt(pt_radius_sq) * Math.cos(pt_angle);
var pt_y = Math.sqrt(pt_radius_sq) * Math.sin(pt_angle);
square(
Math.sqrt(pt_radius_sq) * Math.cos(pt_angle) + cx,
(Math.sqrt(pt_radius_sq) * Math.sin(pt_angle)) / (w / h) + cy,
2,
2
);
count--;
}
}
fill("#CCCCCC");
ellipse(cx, cy, w, h);
dots();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
If anyone can see what's throwing the scale off for the p5.js version and wouldn't mind pointing me in the right direction, I'd be most appreciative.
The function you are using to draw the ellipse takes different parameters in the 1st and 2nd case. In one case it's taking the radiuses, and in the other the diameters, so you're off by a factor of 0.5.
I've simplified your code to consider just circles and kept just the w variable, but you can use the code below and add back the h variable if you wish to.
var count = 300, // number of random points
cx = 300,
cy = 200,
w = 50,
radius = w / 2;
function setup() {
createCanvas(800, 800);
function dots() {
fill('green');
stroke('green');
while (count) {
const pt_angle = Math.random() * 2 * Math.PI;
const px = Math.random() * radius * cos(pt_angle);
const py = Math.random() * radius * sin(pt_angle);
square(
px + cx,
py + cy,
2,
2
);
count--;
}
}
fill("#CCCCCC");
ellipse(cx, cy, w, w);
dots();
}
I am now making Customize roulette, and players can input the text what they wanted.
when I click button, the renderRoulette function is work. And this is the inner context of renderRoulette function
rouletteCanvas.style.display = "block"; // the initial state of rouletteCanvas's display is 'none'
// #customize elements are the inputs of the text that users made.
let customize = document.querySelectorAll("#customize");
let len = customize.length;
const canvas = document.querySelector(".roulette-panel"); // I want to draw here.
let width = canvas.width;
let height = canvas.height;
const ctx = canvas.getContext("2d");
// ctx Initialization
ctx.clearRect(0, 0, width, height);
const devide = len;
const degree = 360;
const goalDegree = 270 + degree / devide;
for (let i = 0; i < devide; i++) {
let json = {
first: (degree / devide) * i,
last: (degree / devide) * (i + 1),
text: `${i + 1}`,
};
data.push(json);
}
// Draw a circular sector
data.forEach((item) => {
ctx.save();
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = "gray";
ctx.moveTo(width / 2, height / 2);
ctx.arc(
width / 2,
height / 2,
width / 2,
degreesToRadians(item.first),
degreesToRadians(item.last),
false
);
ctx.closePath();
ctx.stroke();
ctx.restore();
ctx.save();
let half = Math.abs(item.first - item.last) / 2;
let degg = item.first + half;
let xx = ((Math.cos(degreesToRadians(degg)) * width) / 2) * 0.7 + width / 2;
let yy =
((Math.sin(degreesToRadians(degg)) * width) / 2) * 0.7 + height / 2;
let minus = ctx.measureText(item.text).width / 2;
ctx.font = "bold 15px sans-serif";
ctx.fillText(item.text, xx - minus, yy);
ctx.restore();
});
Every time I click the button, I want to delete all the previously drawn circular sector shapes and redraw them, so I used the code, ctx.clearRect(0, 0, width, height); . However, even if you click the button after, it will be drawn over the existing picture. How do I initialize it?
I get a feeling that the save and restore in your code are causing some problems for you, I tried a simpler version of your code and the ctx.clearRect(0, 0, width, height) works just fine.
Here is the code:
const canvas = document.querySelector(".roulette-panel");
const ctx = canvas.getContext("2d");
let width = height = 100;
const devide = 8;
let data = []
for (let i = 0; i < devide; i++) {
data.push({
first: (360 / devide) * i,
last: (360 / devide) * (i + 1)
});
}
function draw() {
ctx.clearRect(0, 0, width, height);
ctx.beginPath();
data.forEach((item) => {
ctx.moveTo(width / 2, height / 2);
ctx.arc(50, 50, 45,
item.first * Math.PI / 180,
item.last * Math.PI / 180
);
item.first += 0.5
item.last += 0.5
});
ctx.stroke();
}
setInterval(draw, 40)
<canvas class="roulette-panel"></canvas>
This code uses setInterval to delete all the previously drawn circular sector shapes and draw new one on a new position creating the effect of movement
Here is that same code but with the clearRect commented:
const canvas = document.querySelector(".roulette-panel");
const ctx = canvas.getContext("2d");
let width = height = 100;
const devide = 8;
let data = []
for (let i = 0; i < devide; i++) {
data.push({
first: (360 / devide) * i,
last: (360 / devide) * (i + 1)
});
}
function draw() {
//ctx.clearRect(0, 0, width, height);
ctx.beginPath();
data.forEach((item) => {
ctx.moveTo(width / 2, height / 2);
ctx.arc(50, 50, 45,
item.first * Math.PI / 180,
item.last * Math.PI / 180
);
item.first += 0.5
item.last += 0.5
});
ctx.stroke();
}
setInterval(draw, 40)
<canvas class="roulette-panel"></canvas>
We can clearly see a different effect
I've created a canvas pattern and want to repeat the pattern diagonally through the whole web page. Since the repeat property only support repeat-x, repeat-y or repeat both direction, I've set it to 'no-repeat' for now and tried to use offset or translate to move my pattern diagonally, but didn't work out.
Here is what I got for now:
enter image description here
Here is what I want to accomplish:
enter image description here
I'm just trying to mimic the effect, doesn't need to be exact the same.
Can someone tell me how to continue my pattern diagonally? Thanks a lot!
Here are some of my codes:
var patternCanvas = document.createElement('canvas');
var patternContext = patternCanvas.getContext('2d');
patternCanvas.width = 350;
patternCanvas.height = 350;
patternContext.fillStyle = "orange";
patternContext.fillRect(0, 50, 150, 50);
patternContext.fillRect(50, 0, 50, 150);
patternContext.fillStyle = "black";
patternContext.fillRect(100, 100, 150, 50);
patternContext.fillRect(150, 50, 50, 150);
patternContext.fillStyle = "green";
patternContext.fillRect(200, 150, 150, 50);
patternContext.fillRect(250, 100, 50, 150);
patternContext.fillStyle = "darkred";
patternContext.fillRect(0, 100, 50, 150);
patternContext.fillRect(0, 150, 150, 50);
patternContext.fillStyle = "blue";
patternContext.fillRect(100, 150, 50, 150);
patternContext.fillRect(50, 200, 150, 50);
patternContext.fillStyle = "yellow";
patternContext.fillRect(200, 200, 50, 150);
patternContext.fillRect(150, 250, 150, 50);
var canvas = document.getElementById("myCanvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var context = canvas.getContext('2d');
var pattern = context.createPattern(patternCanvas, 'no-repeat');
context.fillStyle = pattern;
context.fillRect(0, 0, 350, 350);
<canvas id="myCanvas"></canvas>
This solution treats the canvas as a blank slate rather than as a place to copy/paste a pattern.
It creates the appearance of a repeating pattern by dividing the canvas into squares and coloring each square according to its position in a virtual grid.
Thanks to irchans for help with the math.
const
// Creates a canvas, and a context
canvas = document.getElementById("myCanvas"),
context = canvas.getContext('2d'),
// Configures colors, unit-square size, and the number of unit squares to draw
colors = "blue,yellow,darkred,green,orange,black".split(","),
unit = 50,
gridDimensionX = 10,
gridDimensionY = 10;
// Makes the canvas wide enough for its content
canvas.width = unit * gridDimensionX;
canvas.height = unit * gridDimensionY;
// Builds a grid of squares, each of which is assigned a color
const grid = makeGrid(gridDimensionX, gridDimensionY, colors);
// Loops through the grid and draws each square
drawGrid(grid, context, unit);
// Defines the `makeGrid` function
function makeGrid(gridDimensionX, gridDimensionY, colors){
const grid = [];
for(let y = 0; y < gridDimensionY; y++){
const row = [];
for(let x = 0; x < gridDimensionX; x++){
// Assigns coordinates to each not-yet-drawn square, along two axes
// (rotated 60 degrees from the vertical and horizontal axes)
// and groups squares according to these coordinates
cell = {
slantyRowGrp: Math.round((2 * y - x) / 5, 0),
slantyColGrp: Math.round((y + 2 * x) / 5, 0)
}
// Assigns a color to each square based on its 'slanty' grouping
cell.colorIndex = (cell.slantyRowGrp + 2 * cell.slantyColGrp) % colors.length;
cell.color = colors[cell.colorIndex];
// Adds the cell to the row
row.push(cell);
}
// Adds the completed row to the grid
grid.push(row);
}
// Returns the completed grid
return grid;
}
// Defines the `drawGrid` function
function drawGrid(grid, context, unit){
grid.forEach( (row, y) => {
row.forEach( (cell, x) => {
// Fills each square with its assigned color
context.fillStyle = cell.color;
context.fillRect(unit * x, unit * y, unit, unit);
// Displays the 'slanty' row and column group numbers
/*
context.fillStyle = "lightgrey";
context.fillText(
`${cell.slantyRowGrp}; ${cell.slantyColGrp}`,
unit * x + unit/2.5,
unit * y + unit/2
);
*/
});
});
}
<canvas id="myCanvas"></canvas>
It took quite a lot of effort to achieve. It's a deceptively complex question.
Each position moves by something like x = x + (iterationY/3) * 2, y = iterationX although my code has denormalized iteration steps for ix and iy of 1/3, to make it easier to reason about moving by cross blocks e.g. 1/3 width or height of a cross.
To assign an id to each cross for colouring, I take a row and column where x/y iterations have a step of 1 as row = iterationX % 2 and col = iterationY % 3, which gives row going from 0, 1 and so on, and col going from 0, 1, 2 and repeating. In this case I assign col a weight of 2, so the id = row+(col*2), to ensure each id is unique. Finally, I define an array of colours which can be referenced by this id.
const canvas = document.getElementById("canv");
const ctx = canvas.getContext('2d');
const w = window.innerWidth;
const h = window.innerHeight;
const s = 80;//size
const bs = s / 3;//block size
const gs = Math.max(w, h)/s;//grid size
canvas.width = w;
canvas.height = h;
let yAcc = 0;
let jx = 0;
let jy = 0;
const getColour = (jx, jy) => {
const row = (jx%2);//0, 1
const col = (jy % 3);//0, 1, 2
//00, 10, 01, 11, 02, 12
//0 , 1 , 2 , 3 , 4 , 5
const id = row + (col*2);
const colours = ['orange', 'blue', 'black', 'yellow', 'green', 'red']
return colours[id];
}
for(let ix = 0; ix < gs; ix+=1/3){
for(let iy = 0; iy < gs; iy+=1/3){
const colour = getColour(jx, jy);
let x = ix+iy*2-(gs);
let y = iy+ix*3-(gs);
ctx.fillStyle = colour;
ctx.beginPath();
ctx.lineWidth = 2;
ctx.rect(x * bs * 3, y * bs * 3 + bs, bs * 3, bs);
ctx.rect(x * bs * 3 + bs, y * bs * 3, bs, bs * 3);
ctx.fill();
ctx.closePath();
jy ++;
}
jx ++;
}
<canvas id="canv"></canvas>
I'm trying to create a little circular "equalizer" effect using JavaScript and HTML canvas for a little project I'm working on, and it works great, except one little thing. It's just a series of rectangular bars moving in time to an mp3 - nothing overly fancy, but at the moment all the bars point in one direction (i.e. 0 radians, or 90 degrees).
I want each respective rectangle around the edge of the circle to point directly away from the center point, rather than to the right. I have 360 bars, so naturally, each one should be 1 degree more rotated than the previous.
I thought that doing angle = i*Math.PI/180 would fix that, but it doesn't seem to matter what I do with the rotate function - they always end up pointing in weird and wonderful directions, and being translated a million miles from where they were. And I can't see why. Can anyone see where I'm going wrong?
My frame code, for reference, is as follows:
function frames() {
// Clear the canvas and get the mp3 array
window.webkitRequestAnimationFrame(frames);
musicArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(musicArray);
ctx.clearRect(0, 0, canvas.width, canvas.height);
bars = 360;
for (var i = 0; i < bars; i++) {
// Find the rectangle's position on circle edge
distance = 100;
var angle = i * ((Math.PI * 2) / bars);
var x = Math.cos(angle) * distance + (canvas.width / 2);
var y = Math.sin(angle) * distance + (canvas.height / 2);
barWidth = 5;
barHeight = (musicArray[i] / 4);
// Fill with a blue-green gradient
var grd = ctx.createLinearGradient(x, 0, x + 40, 0);
grd.addColorStop(0, "#00CCFF");
grd.addColorStop(1, "#00FF7F");
ctx.fillStyle = grd;
// Rotate the rectangle according to position
// ctx.rotate(i*Math.PI/180); - DOESN'T WORK
// Draw the rectangle
ctx.fillRect(x, y, barHeight, barWidth);
}
For clarity I've removed part of your code. I'm using rotate as you intended. Also I'm using barHeight = (Math.random()* 50); instead your (musicArray[i]/4); because I wanted to have something to show.
Also I've changed your bars to 180. It's very probable that you won't have 360 bars but 32 or 64 or 128 or 256 . . . Now you can change the numbers of bare to one of these numbers to see the result.
I'm drawing everything around the origin of the canvas and translating the context in the center.
I hope it helps.
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
let cw = canvas.width = 400;
let ch = canvas.height = 400;
let bars = 180;
let r = 100;
ctx.translate(cw / 2, ch / 2)
for (var i = 0; i < 360; i += (360 / bars)) {
// Find the rectangle's position on circle edge
var angle = i * ((Math.PI * 2) / bars);
//var x = Math.cos(angle)*r+(canvas.width/2);
//var y = Math.sin(angle)*r+(canvas.height/2);
barWidth = 2 * Math.PI * r / bars;
barHeight = (Math.random() * 50);
ctx.fillStyle = "green";
// Rotate the rectangle according to position
// ctx.rotate(i*Math.PI/180); - DOESN'T WORK
// Draw the rectangle
ctx.save();
ctx.rotate(i * Math.PI / 180);
ctx.fillRect(r, -barWidth / 2, barHeight, barWidth);
//ctx.fillRect(r ,0, barHeight, barWidth);
ctx.restore();
}
canvas {
border: 1px solid
}
<canvas id="c"></canvas>
Here is another solution, I'm preserving your initial trigonometry approach.
But instead of rectangles I used lines, I don't think it makes a difference for you, if what you need is bars moving in time to an mp3 all you need to do is change the var v = Math.random() + 1; to a reading from the Amplitude, and those bars will be dancing.
const canvas = document.getElementById("c");
canvas.width = canvas.height = 170;
const ctx = canvas.getContext("2d");
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.lineWidth = 2;
let r = 40;
let bars = 180;
function draw() {
ctx.clearRect(-100, -100, 200, 200)
for (var i = 0; i < 360; i += (360 / bars)) {
var angle = i * ((Math.PI * 2) / bars);
var x = Math.cos(angle) * r;
var y = Math.sin(angle) * r;
ctx.beginPath();
var v = Math.random() + 1;
ctx.moveTo(x, y);
ctx.lineTo(x * v, y * v)
grd = ctx.createLinearGradient(x, y, x*2, y*2);
grd.addColorStop(0, "blue");
grd.addColorStop(1, "red");
ctx.strokeStyle = grd;
ctx.stroke();
}
}
setInterval(draw, 100)
<canvas id="c"></canvas>
I'm using PHP to create a foreach loop for stats using HTML5 Canvas. Now everything is working great except the JavaSscript I found to create the Canvas Graph, it gets an element by id and so only manually targets one item, not sure how I can change up this code to target any amount of items created by the PhP foreach loop.
Here is the JavaScript I found along with the HTML element (ignore the PhP variables in the data-attributes, they are returning data):
var el = document.getElementById('graph'); // get canvas
//var el = document.getElementsByClassName("chart");
var options = {
percent: el.getAttribute('data-percent') || 25,
color: el.getAttribute('data-color') || 0,
size: el.getAttribute('data-size') || 90,
lineWidth: el.getAttribute('data-line') || 3,
rotate: el.getAttribute('data-rotate') || 0
}
var canvas = document.createElement('canvas');
var span = document.createElement('span');
span.textContent = options.percent + '%';
if (typeof(G_vmlCanvasManager) !== 'undefined') {
G_vmlCanvasManager.initElement(canvas);
}
var ctx = canvas.getContext('2d');
canvas.width = canvas.height = options.size;
el.appendChild(span);
el.appendChild(canvas);
ctx.translate(options.size / 2, options.size / 2); // change center
ctx.rotate((-1 / 2 + options.rotate / 180) * Math.PI); // rotate -90 deg
//imd = ctx.getImageData(0, 0, 240, 240);
var radius = (options.size - options.lineWidth) / 2;
var drawCircle = function(color, lineWidth, percent) {
percent = Math.min(Math.max(0, percent || 1), 1);
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2 * percent, false);
ctx.strokeStyle = color;
ctx.lineCap = 'round'; // butt, round or square
ctx.lineWidth = lineWidth
ctx.stroke();
};
drawCircle('#eeeeee', 1, 100 / 100);
drawCircle(options.color, options.lineWidth, options.percent / 100);
<div class="chart" id="graph" data-percent="<?php echo $percentage; ?>" data-color="<?php echo $color; ?>" style="color: <?php echo $color; ?>;"></div>
using get element by class name returns an error: Uncaught TypeError: el.getAttribute is not a function
Ok, so getElementsByClassName returns a NodeList object. What you need to do is turn it into an array and loop over all it's elements and execute your code for each of them. Here is a minimally-changed version of how your code should work:
// Get all the elements with 'chart' class as a NodeList object
var elements = document.getElementsByClassName("chart");
// Turn the NodeList into an array
elements = Array.prototype.slice.call(elements);
// Loop over all the elements
elements.forEach(function (el) {
var options = {
percent: el.getAttribute('data-percent') || 25,
color: el.getAttribute('data-color') || 0,
size: el.getAttribute('data-size') || 90,
lineWidth: el.getAttribute('data-line') || 3,
rotate: el.getAttribute('data-rotate') || 0
}
var canvas = document.createElement('canvas');
var span = document.createElement('span');
span.textContent = options.percent + '%';
if (typeof(G_vmlCanvasManager) !== 'undefined') {
G_vmlCanvasManager.initElement(canvas);
}
var ctx = canvas.getContext('2d');
canvas.width = canvas.height = options.size;
el.appendChild(span);
el.appendChild(canvas);
ctx.translate(options.size / 2, options.size / 2); // change center
ctx.rotate((-1 / 2 + options.rotate / 180) * Math.PI); // rotate -90 deg
//imd = ctx.getImageData(0, 0, 240, 240);
var radius = (options.size - options.lineWidth) / 2;
var drawCircle = function(color, lineWidth, percent) {
percent = Math.min(Math.max(0, percent || 1), 1);
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2 * percent, false);
ctx.strokeStyle = color;
ctx.lineCap = 'round'; // butt, round or square
ctx.lineWidth = lineWidth
ctx.stroke();
};
drawCircle('#eeeeee', 1, 100 / 100);
drawCircle(options.color, options.lineWidth, options.percent / 100);
});
<div class="chart" data-percent="100" data-color="red" style="color: red;"></div>
<div class="chart" data-percent="30" data-color="red" style="color: red;"></div>
However, this code should definitely be refactored. For instance, there is no point in declaring drawCircle function on every iteration.
Also read up on getElementByClassName, and forEach.