Im trying to generate a simple pie chart using canvas, but its not displaying anything when data is assigned to an array used to draw the chart. I cant seem to find out exactly what the issue is.
Ive tried to convert the array to integers using a loop but it still won't use the data to draw the graph.
function draw() {
var n = document.getElementById("data").value;
var values = n.split(",");
//document.write(values);
var colors = [ "blue", "red", "yellow", "green", "black" ];
var canvas = document.getElementById( "myCanvas");
var context = canvas.getContext( "2d" );
// grey background
context.fillStyle = "rgb(200,200,200)";
context.fillRect (0, 0, 600, 450);
//iterate through array and assign values to integers
for( var i = 0; i < values.length; i++ ){
values[i] = parseInt(values[i]);
}
// get length of data array
var dataLength = values.length;
// variable to store all values
var total = 0;
// calculate total
for( var i = 0; i < dataLength; i++ ){
// add data value to total
total += values[ i ];
}
// declare X and Y coordinates
var x = 100;
var y = 100;
var radius = 100;
// declare starting point
var startingPoint = 0;
for( var i = 0; i < dataLength; i++ ){
// calculate percent of total for current value
var percent = values[ i ][ 1 ] * 100 / total;
// calculate end point using the percentage (2 is the final point for the chart)
var endPoint = startingPoint + ( 2 / 100 * percent );
// draw chart segment for current element
context.beginPath();
// select corresponding color
context.fillStyle = colors[ i ];
context.moveTo( x, y );
context.arc( x, y, radius, startingPoint * Math.PI, endPoint * Math.PI );
context.fill();
// starting point for next element
startingPoint = endPoint;
}
}
<html>
<head>
<title> Easy Graph</title>
<link rel="stylesheet" href="stylesheet.css">
<style type = "text/css">
canvas {
border: 2px solid black;
}
</style>
<script type="text/javascript" src="pie.js">
</script>
</head>
<body>
<h2> Pie chart </h2>
<h2> Enter values seperated by commas</h2>
<input type="text" name="number" id="data"><br>
<input type="button" value="submit" name="submit" onclick = "draw();">
<input type="button" value="Clear" name="Clear" onclick="reset()"><br><br>
<canvas id ="myCanvas" width= "600" height= "300">
</canvas>
</body>
</html>
This like is wrong:
var percent = values[ i ][ 1 ] * 100 / total;
It should be this way
var percent = values[ i ] * 100 / total;
You're trying to get a position in the array that doesn't exist.
Related
I want to create an element, where the end-user can enter its information, and the result is shown in a pie chart.
I have borrowed a pre-made pie chart, and when inserting the data as fixed numbers, there is no issue, it shows the right distribution.
However when adding a user input in this example a range type to substitute one of the data points, it is not showing the results. What am I missing here?
<canvas id="test_canvas" width="400" height="250"></canvas>
<input id="danmark" type="range" min="1000" max ="3000" step="100" placeholder ="2500" value="2500" oninput="myFunction()" >
<script>
// pie chart drawings
function drawPieChart( data, colors, title ){
var canvas = document.getElementById( "test_canvas" );
var context = canvas.getContext( "2d" );
// get length of data array
var dataLength = data.length;
// declare variable to store the total of all values
var total = 0;
// calculate total
for( var i = 0; i < dataLength; i++ ){
// add data value to total
total += data[ i ][ 1 ];
}
// declare X and Y coordinates of the mid-point and radius
var x = 100;
var y = 100;
var radius = 100;
// declare starting point (right of circle)
var startingPoint = 0;
for( var i = 0; i < dataLength; i++ ){
// calculate percent of total for current value
var percent = data[ i ][ 1 ] * 100 / total;
// calculate end point using the percentage (2 is the final point for the chart)
var endPoint = startingPoint + ( 2 / 100 * percent );
// draw chart segment for current element
context.beginPath();
// select corresponding color
context.fillStyle = colors[ i ];
context.moveTo( x, y );
context.arc( x, y, radius, startingPoint * Math.PI, endPoint * Math.PI );
context.fill();
// starting point for next element
startingPoint = endPoint;
// draw labels for each element
context.rect( 220, 25 * i, 15, 15 );
context.fill();
context.fillStyle = "black";
context.fillText( data[ i ][ 0 ] + " (" + data[ i ][ 1 ] + ")", 245, 25 * i + 15 );
}
// draw title
context.font = "20px Arial";
context.textAlign = "center";
context.fillText( title, 100, 225 );
}
function myFunction () {
var Danmark = document.getElementbyId('danmark').value;
var Norge = 850;
var Sverige = 2400;
var Finland = 1000;
var Holland = 3000;
}
// the data, which needs to be calculated based on user input variables.
var data = [ [ "Danmark", Danmark ], [ "NL", Holland ], [ "NO", Norge ], [ "SE", Sverige ], [ "FI", Finland ] ];
var colors = [ "red", "orange", "green", "blue", "purple" ];
// using the function
drawPieChart( data, colors, "Sales" );
</script>
```
You have a typo in
var Danmark = document.getElementbyId('danmark').value;
It should be getElementById not getElementbyId.
Another thing. When you get value from input or textarea it returns string value by default. You have to convert that value in the number format.
let Danmark = document.getElementById('danmark').value;
let converted = parseInt(Danmark);
function checkVals() {
console.log(typeof Danmark);
console.log(typeof converted);
}
<textarea id="danmark" onchange="checkVals()"></textarea>
At the beginning of your drawPieChart function, under your context variable you'll need to add context.clearRect(0, 0, canvas.width, canvas.height);. This will clear the canvas for the next draw, otherwise it will just draw on top of what's already there and you won't be able to see the changes.
So it will look like this:
function drawPieChart( data, colors, title ){
var canvas = document.getElementById( "test_canvas" );
var context = canvas.getContext( "2d" );
context.clearRect(0, 0, canvas.width, canvas.height);
I am trying to create some scales measuring sound frequency for a music visualiser project. They are meant to display 4 different frequencies ( bass, lowMid, highMid and treble in a 2x2 grid pattern. I'm nearly there I have my rectangles but the needle which measures and shows the frequency itself is only iterating for the top row x and not the bottom row. I'm pretty new to JavaScript so I'm sure it could be something very simple that I'm missing.
// draw the plots to the screen
this.draw = function() {
//create an array amplitude values from the fft.
var spectrum = fourier.analyze();
//iterator for selecting frequency bin.
var currentBin = 0;
push();
fill('#f0f2d2');
//nested for loop to place plots in 2*2 grid.
for(var i = 0; i < this.plotsDown; i++) {
for(var j = 0; j < this.plotsAcross; j++) {
//calculate the size of the plots
var x = this.pad * j * 10;
var y = height/20 * i * 10;
var w = (width - this.pad) / this.plotsAcross;
var h = (height - this.pad) / this.plotsDown;
//draw a rectangle at that location and size
rect(x, y, w, h);
//add on the ticks
this.ticks((x + w/2), h, this.frequencyBins[i])
var energy = fourier.getEnergy(this.frequencyBins[currentBin]);
//add the needle
this.needle(energy, (x + w/2), h)
currentBin++;
}
}
pop();
};
I have a following task that I'm trying to accomplish the most efficient way possible: I have varying number of pictures of varying size as pixel arrays that I need to add to canvas pixel by pixel. Each pixel's value has to be added to canvas's ImageData so that the result is a blend of two or more images.
My current solution is to retrieve ImageData from the location where the picture needs to be blended with the size of the picture. Then I add the picture's ImageData to the retrieved ImageData and copy it back to the same location.
In a sense this is a manual implementation of canvas globalCompositeOperation "lighter".
"use strict";
let canvas = document.getElementById("canvas");
let width = canvas.width = window.innerWidth;
let height = canvas.height = window.innerHeight;
let ctx = canvas.getContext("2d");
ctx.fillStyle="black";
ctx.fillRect(0, 0, width, height);
let imageData = ctx.getImageData(0,0,width,height);
let data = imageData.data;
function random(min, max) {
let num = Math.floor(Math.random() * (max - min + 1)) + min;
return num;
}
function createColorArray(size, color) {
let arrayLength = (size*size)*4;
let array = new Uint8ClampedArray(arrayLength);
for (let i = 0; i < arrayLength; i+=4) {
switch (color) {
case 1:
array[i+0] = 255; // r
array[i+1] = 0; // g
array[i+2] = 0; // b
array[i+3] = 255; // a
break;
case 2:
array[i+0] = 0; // r
array[i+1] = 255; // g
array[i+2] = 0; // b
array[i+3] = 255; // a
break;
case 3:
array[i+0] = 0; // r
array[i+1] = 0; // g
array[i+2] = 255; // b
array[i+3] = 255; // a
}
}
return array;
}
function picture() {
this.size = random(10, 500);
this.x = random(0, width);
this.y = random(0, height);
this.color = random(1,3);
this.colorArray = createColorArray(this.size, this.color);
}
picture.prototype.updatePixels = function() {
let imageData = ctx.getImageData(this.x, this.y, this.size, this.size);
let data = imageData.data;
for (let i = 0; i < data.length; ++i) {
data[i]+=this.colorArray[i];
}
ctx.putImageData(imageData, this.x, this.y);
}
let pictures = [];
let numPictures = 50;
for (let i = 0; i < numPictures; ++i) {
let pic = new picture();
pictures.push(pic);
}
function drawPictures() {
for (let i = 0; i < pictures.length; ++i) {
pictures[i].updatePixels();
}
}
drawPictures();
<!DOCTYPE html>
<html>
<head>
<title>...</title>
<style type="text/css">
body {margin: 0px}
#canvas {position: absolute}
</style>
</head>
<body>
<div>
<canvas id="canvas"></canvas>
</div>
<script type="text/javascript" src="js\script.js"></script>
</body>
</html>
This solution works fine but it's very slow. I don't know if pixel by pixel blending can even be made very efficient, but one reason for slow performance might be that I need to get the ImageData and put it back each time a new image is blended into canvas.
Therefore the main question is how could I get whole canvas ImageData once in the beginning and then look correct pixels to update based on location and size of each picture that needs to blended into canvas and finally put updated ImageData back to canvas? Also, any other ideas on how to make blending more efficient are greatly appreciated.
Use the array methods.
The fastest way to fill an array is with the Array.fill function
const colors = new Uint32Array([0xFF0000FF,0xFF00FF00,0xFFFF00]); // red, green, blue
function createColorArray(size, color) {
const array32 = new Uint32Array(size*size);
array32.fill(colors[color]);
return array32;
}
Quick clamped add with |
If you are adding 0xFF to any channel and 0 to the others you can use | and a 32 bit array. For the updatePixels function
var imageData = ctx.getImageData(this.x, this.y, this.size, this.size);
var data = new Uint32Array(imageData.data.buffer);
var i = 0;
var pic = this.colorArray; // use a local scope for faster access
do{
data[i] |= pic[i] ; // only works for 0 and FF chanel values
}while(++i < data.length);
ctx.putImageData(imageData, this.x, this.y);
Bitwise or | is similar to arithmetic add and can be used to increase values using 32bit words. The values will be clamped as part of the bitwise operation.
// dark
var red = 0xFF000088;
var green = 0xFF008800;
var yellow = red | green; // 0xFF008888
There are many other ways to use 32bit operations to increase performance as long as you use only 1 or 2 operators. More and you are better off using bytes.
You can also add if you know that each channel will not overflow a bit
a = 0xFF101010; // very dark gray
b = 0xFF000080; // dark red
// non overflowing add
c = a + b; // result is 0xFF000090 correct
// as 0x90 + 0x80 will overflow = 0x110 the add will not work
c += b; // result overflows bit to green 0xFF000110 // incorrect
Uint8Array V Uint8ClampedArray
Uint8Array is slightly faster than Uint8ClampedArray as the clamping is skipped for the Uint8Array so use it if you don't need to clamp the result. Also the int typedArrays do not need you to round values when assigning to them.
var data = Uint8Array(1);
data[0] = Math.random() * 255; // will floor for you
var data = Uint8Array(1);
data[0] = 256; // result is 0
data[0] = -1; // result is 255
var data = Uint8ClampedArray(1);
data[0] = 256; // result is 255
data[0] = -1; // result is 0
You can copy data from array to array
var imageDataSource = // some other source
var dataS = new Uint32Array(imageData.data.buffer);
var imageData = ctx.getImageData(this.x, this.y, this.size, this.size);
var data = new Uint32Array(imageData.data.buffer);
data.set(dataS); // copies all data
// or to copy a row of pixels
// from coords
var x = 10;
var y = 10;
var width = 20; // number of pixels to copy
// to coords
var xx = 30
var yy = 30
var start = y * this.size + x;
data.set(dataS.subArray(start, start + width), xx + yy * this.size);
Dont dump buffers
Don't keep fetching pixel data if not needed. If it does not change between putImageData and getImageData then there is no need to get the data again. It is better to keep the one buffer than continuously creating a new one. This will also relieve the memory stress and reduce the workload on GC.
Are you sure you can not use the GPU
And you can perform a wide range of operations on pixel data using global composite operations. Add, subtract, multiply, divide, invert These are much faster and so far in your code I can see no reason why you need to access the pixel data.
I want to make a box to move as a sinusoidal graph.
At the point where i am now i simply can't represent the box into the canvas. At the beginning I was able to, but after working out the trigonometry part the box disappeared and a get no error...
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<canvas id="canvas" width="600" height="300" style="background-color:red"></canvas>
<script type="text/javascript">
var canvas = document.querySelector("canvas");//isoute me document.getElementsByTagName()
var ctx = canvas.getContext("2d");
var can_width = canvas.width;
var can_height = canvas.height;
var x,y;
function PVector(x_,y_){
var y = [];
var x = [0, Math.PI/6, Math.PI/4, Math.PI/3, Math.PI/2, 2/3*Math.PI, 3/4*Math.PI,
5/6*Math.PI, Math.PI, 7/6*Math.PI, 5/4*Math.PI, 4/3*Math.PI, 3/2*Math.PI,
5/3*Math.PI, 7/4*Math.PI, 11/6*Math.PI, 2*Math.PI];
for (var i=0, len=x["length"]; i<len; i++){
var A;
A = Math.sin(x[i]);
y.push(A);
}console.log(y);console.log(x);
return{
x:x,
y:y
};
}
var Point = {
location : {x:0, y: can_height/2},//initial location
velocity : new PVector(x,y),
display : ctx.fillRect(can_width/2,can_height/2 , 25, 25),//initial position of the box
step : function(){
this.location.x += this.velocity.x;
this.location.y += this.velocity.y;
},
display : function(){
ctx.fillRect(this.location.x, this.location.y, 25, 25);
}
};
function update(){
Point["step"]();
ctx.clearRect(0,0, can_width, can_height);
Point["display"]();
window.setTimeout(update, 1000/30);
}
function init(){
update();
}
init();
</script>
</body>
Problem
In your PVector object you are returning Arrays for x and y, while you use them as values in the step() method. This will cause the entire array to be added as a string.
Solution
You need something that traverse that array. Here is an example, it may not be the result you're after, but it shows the principle which you need to apply:
// first note the return type here:
function PVector(x_,y_){
var y = [];
var x = [0, Math.PI/6, Math.PI/4, Math.PI/3, Math.PI/2, 2/3*Math.PI,
...snipped---
return {
x:x, // x and y are arrays
y:y
};
}
var Point = {
location: {
x: 0,
y: can_height / 2,
step: 0 // some counter to keep track of array position
}, //initial location
velocity: new PVector(x, y),
step: function () {
this.location.step++; // increase step for arrays
// using modulo will keep the step as a valid value within the array length:
// if step = 7 and length = 5, index will become 2 (sort of "wrap around")
var indexX = this.location.step % this.velocity.x.length;
var indexY = this.location.step % this.velocity.y.length
this.location.x += this.velocity.x[indexX];
this.location.y += this.velocity.y[indexY];
},
...
Updated fiddle
Tip: I would as Robin in his answer, recommend to simplify the sinus calculation. Sinus-tables are good when performance is needed and the browser can't keep up (ie. will thousands of objects), but in simpler scenario, direct calculation will work too.
If your goal is just to have a box moving in a sinusoidal graph, it can be done simpler.
This jsfiddle shows a slightly simpler example of a box moving in a sinusoidal graph where I just removed parts of your code and calculate the path with Math.sin and use time instead of precalculated values for x.
function update(){
time += 0.1;
ctx.clearRect(0,0, can_width, can_height);
x = time;
y = (can_height/2)+(can_height/2)*Math.sin(time);
console.log(x, y);
ctx.fillRect(x*16, y, 25, 25);
window.setTimeout(update, 1000/30);
}
The variables are modified to make it look ok on the canvas. You can edit the addition to time, and the altitude and base line for y, to fit your needs.
If you need to follow the specification in your code, look at the answer by Ken.
Since you want a sinusoidal move, it makes sense to use... the sin function.
The formula for a sinusoidal move is :
y = maxValue * sin ( 2 * PI * frequency * time ) ;
where the frequency is in Herz (== 'number of times per second') and time is in second.
Most likely you'll use Date.now(), so you'll have a time in millisecond, that you need to convert. Since the value of PI should not change in the near future, you can compute once the magic number
k = 2 * PI / 1000 = 0.006283185307179587
and the formula becomes :
y = sin( k * frequency * Date.now() );
Here's a simple example on how to use the formula :
var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");
var can_width = canvas.width;
var can_height = canvas.height;
// magic number
var k = 0.006283185307179587;
// oscillations per second
var frequency = 1/5;
// ...
var amplitude = can_width / 8;
function animate() {
ctx.clearRect(0,0,can_width, can_height);
ctx.fillStyle= '#000';
var y = amplitude * Math.sin ( k * frequency * Date.now() );
ctx.fillRect(can_width/2, can_height/2 + y, 20, 20 );
}
setInterval(animate, 30);
<canvas id="canvas" width="400" height="200" style="background-color:red"></canvas>
I'm experimenting with some html 5 canvas and at a certain point I tried to update the canvas with new bars (rectangles).
Now the tricky part.
Lets say you have an array with 1800 items(numbers generated by a php request).
You also have an html 5 canvas with the width of 1163px.
Now you need to draw bars(rectangles) in the canvas, but the bars need to remain 2px wide and there must be a 1px margin between all bars.
So the PHP file always returns 1800 numbers (color codes from image) and you need to extract just enough numbers to fit the canvas width(with the margin included(bar+margin)).
The new array of numbers you extract cannot just be the first 100-200 numbers! It must contain the (first or second) and (last or last-1) number.
What I have tried so far
<script>
// 1800 numbers !!! ......... //
arr = [12,1,21,1,2,13,21,32,1,5,4,6,5,4,6,4,8,3,5,4,9,6,8,7,9,1,3,6,4,6,4,5,9,87,4,4,5];
c = getElementById('canvas');
ctx = c.getContex('2d');
var can = 360;
var spliter = 2;
var canWidth = c.width;
var numOfBars = arr.length;
var barwidth = 2;
var margin = 1;
// amount of bars that will fit in the canvas //
var maxbars = (canWidth / (barwidth + margin));
var offset = arr.length / maxbars;
for(var i = 0; i < arr.length; i++) {
if(arr[i] % offset){
ctx.fillStyle = "#000000";
ctx.fillRect(
this.margin + i * this.barwidth / numOfBars,
0,
barwidth + margin,
100
);
}
}
</script>
This does not seem to work how I need it to work.
Thanks in advance.
Maxbars must be an integer right? and same for offset.
After that offset is your step.
var maxbars = Math.floor(canWidth / (barwidth + margin));
var offset = Math.floor(arr.length / maxbars);
for(var i = 0; i < arr.length; i+=offset) {
var colorcode = arr[i];
// fill stuff
}