I have made a simple graph in a canvas but am having difficulty with two issues.
The first issue is setting the vertical axis with an appropriate scale automatically with enough room for each data value in an array. Ideally i'd like the numbers to be more rounded to the nearest million or thousand etc depending on it's actual value ranges rather than a value like 33145 as the first scale line.
Currently one value is too high for the scale and is not being drawn on the canvas because it is out of bounds.
The second issue, is the points don't seem to be plotting in their correct location, which I am unsure where my mistake was.
I made a JSFiddle as for the most part it might be a bit confusing without seeing it in action:
http://jsfiddle.net/ezttywzr/
This is how i plot my data and draw my vertical axis:
Vertical Axis:
var x = 0,
y,
range = data.max() - data.min(),
valueStep = range / 10,
// get width of largest number
margin = 3 + ctx.measureText(data.min() + (valueStep*10)).width,
pixelStep = (graph.height-40) / 10,
verticalP = pixelStep,
output;
// draw left hand values
for(var i = 0; i < 11; i++){
output = data.min() + (valueStep*i);
y = graph.height-20 - (verticalP + i*pixelStep);
ctx.fillText(output,x,y+6);
ctx.beginPath();
ctx.moveTo(margin, y);
ctx.lineTo(x2,y);
ctx.stroke();
}
Data Plotting:
var y = graph.height,
x = margin,
pos,
valueStep = (graph.width-(margin*2)) / data.length,
pixelRange = graph.height-20,
pp = range / pixelRange;
for(var i = 0; i < data.length; i++){
x += valueStep;
pos = x - (valueStep/2);
ctx.beginPath();
ctx.moveTo(x, graph.height-20);
ctx.lineTo(x, graph.height);
ctx.stroke();
ctx.fillText('Week '+(i+1),pos-(ctx.measureText('Week '+(i+1)).width/2),y);
ctx.beginPath();
ctx.arc(pos,(graph.height-20)-(verticalP+(data[i]/pp)),2,0,2*Math.PI);
ctx.stroke();
ctx.fill();
}
Nice job so far.
I made a few changes: http://jsfiddle.net/ezttywzr/2/
To get the scale I used
STEP = data.max() / NUM_HORIZONTAL_LINES
Where NUM_HORIZONTAL_LINES is the number of horizontal lines you want above the x-axis. In this case I used 10.
This means the first line will be 1 * STEP, the second will be 2 * STEP, the third will be 3 * STEP and so on..
This scale is convenient because it guarantees that the max value fits on the graph. In fact, the max value is on the top line because of the way we defined the scale.
Once we have our scale it's easy to calculate the position of the points relative to the x-axis. It's simply:
(PIXELS_PER_STEP / STEP) * VALUE
To go a step further you can do some math to round the top point of the graph up and pick a scale with that has nice round numbers.
Related
I am trying to use different line width for different axes for parallel coordinates plot using d3.js
It works fine only if its in strictly decreasing order (for eg: lineWidth=10 for 1st axis, lineWidth=5 for 2nd axis and lineWidth=2 for 3rd axis). But if the values are like (lineWidth=10 for 1st axis, lineWidth=2 for 2nd axis and lineWidth=5 for 3rd axis) then for both the second and third axis the line thickness is 5, which is the greater value after a smaller one always overrides the smaller value. Is there any way to handle this issue? Your help is much appreciable.
Below is some code snippet which I'm currently working on.
if (i == 0) {
ctx.beginPath();
ctx.moveTo(x,y);
}
else {
if(i == 1) {
ctx.lineWidth = 1;
}
if(i == 2) {
ctx.lineWidth = 5;
}
if(i == 3) {
ctx.lineWidth = 10;
}
var cp1x = x - 0.55*(x-x0);
var cp1y = y0;
var cp2x = x - 0.45*(x-x0);
var cp2y = y;
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
ctx.stroke();
}
For the above code the line width is 10 for all the axes.
If I use ctx.beginPath() before every if statement, then the line thickness is working fine. But unfortunately the lines drawn between axes are shifting its position (only half-lines are being drawn)
Thanks in advance.
If I use ctx.beginPath() at every if statement then this is what I'm getting
Move
ctx.beginPath();
ctx.moveTo(x,y);
before every if statement. That way each iteration will be a new path and you will move to the correct position to start each line.
I'm making a graph, mostly as an exercise. The graph tries to connect values by lines, but if a value cannot be connected, it just draws a pixel.
In the following example, I made sure that minY, maxY and pixelX are all integer values. They actually come from Int32Array in my real code.
// some min max range for this X pixel coordinate
const minY = data[i].min;
const maxY = data[i].max;
// if there are multiple values on this X coordinate
if (maxY - minY > 1) {
ctx.beginPath();
ctx.moveTo(pixelX + 0.5, minY + 0.5);
ctx.lineTo(pixelX + 0.5, maxY + 0.5);
ctx.closePath();
ctx.stroke();
}
// otherwise just draw a pixel
else {
// if the value was interpolated, it's already colored for this pixel
if (!valueIsInterpolated) {
ctx.strokeRect(pixelX + 0.5, minY + 0.5, 1, 1);
}
}
Of it SHOULD draw a single pixel, but instead, it draws variously shaped rectangles which look REALLY ugly in the graph.
It would look like this if I remove the + 0.5 from the call:
That's even worse. How can I make sure that strokeRect draws EXACTLY ONE pixel? No funny business, no anti-aliasing. Just mark a pixel. Why is this happening?
You're using strokeRect() which will draw an outline of your 1-pixel rectangle meaning you will end up with a half pixel outside in all directions (assuming 1 pixel width of the line) which will need to be anti-aliased.
You'd want to use fillRect() instead which will fill that 1 pixel area.
const ctx = c.getContext("2d");
for(let x = 5; x < c.width; x += 10) ctx.fillRect(x, 5, 1, 1);
<canvas id=c></canvas>
Compared to using strokeRect() which will "bleed" 0.5 (with line width = 1) in all directions from the vector box (which you don't want):
const ctx = c.getContext("2d");
for(let x = 5; x < c.width; x += 10) ctx.strokeRect(x, 5, 1, 1);
<canvas id=c></canvas>
I am currently developing a game, which requires a map consisting of various tile images. I managed to make them display correctly (see second image) but I am now unsure of how to calculate the clicked tile from the mouse position.
Are there any existing libraries for this purpose?
Please also note, that the tile images aren't drawn perfectly "corner-facing-camera", they are slightly rotated clockwise.
Isometric Transformations
Define a projection
Isometric display is the same as standard display, the only thing that has changed is the direction of the x and y axis. Normally the x axis is defined as (1,0) one unit across and zero down and the y axis is (0,1) zero units across and one down. For isometric (strictly speaking your image is a dimetric projection) you will have something like x axis (0.5,1) and y axis (-1,0.5)
The Matrix
From this you can create a rendering matrix with 6 values Two each for both axes and two for the origin, which I will ignore for now (the origin) and just use the 4 for the axis and assume that the origin is always at 0,0
var dimetricMatrix = [0.5,1.0,-1,0.5]; // x and y axis
Matrix transformation
From that you can get a point on the display that matches a given isometric coordinate. Lets say the blocks are 200 by 200 pixels and that you address each block by the block x and y. Thus the block in the bottom of your image is at x = 2 and y = 1 (the first top block is x = 0, y = 0)
Using the matrix we can get the pixel location of the block
var blockW = 200;
var blockH = 200;
var locX = 2;
var locY = 1;
function getLoc(x,y){
var xx,yy; // intermediate results
var m = dimetricMatrix; // short cut to make code readable
x *= blockW; // scale up
y *= blockH;
// now move along the projection x axis
xx = x * m[0];
yy = x * m[1];
// then add the distance along the y axis
xx += y * m[2];
yy += y * m[3];
return {x : xx, y : yy};
}
Befoer I move on you can see that I have scaled the x and y by the block size. We can simplify the above code and include the scale 200,200 in the matrix
var xAxis = [0.5, 1.0];
var yAxis = [-1, 0.5];
var blockW = 200;
var blockH = 200;
// now create the matrix and scale the x and y axis
var dimetricMatrix = [
xAxis[0] * blockW,
xAxis[1] * blockW,
yAxis[0] * blockH,
yAxis[1] * blockH,
]; // x and y axis
The matrix holds the scale in the x and y axis so that the two numbers for x axis tell us the direction and length of a transformed unit.
Simplify function
And redo the getLoc function for speed and efficiency
function transformPoint(point,matrix,result){
if(result === undefined){
result = {};
}
// now move along the projection x axis
result.x = point.x * matrix[0] + point.y * matrix[2];
result.y = point.x * matrix[1] + point.y * matrix[3];
return result;
}
So pass a point and get a transformed point back. The result argument allows you to pass an existing point and that saves having to allocate a new point if you are doing it often.
var point = {x : 2, y : 1};
var screen = transformPoint(point,dimetricMatrix);
// result is the screen location of the block
// next time
screen = transformPoint(point,dimetricMatrix,screen); // pass the screen obj
// to avoid those too
// GC hits that kill
// game frame rates
Inverting the Matrix
All that is handy but you need the reverse of what we just did. Luckily the way matrices work allows us to reverse the process by inverting the matrix.
function invertMatrix(matrix){
var m = matrix; // shortcut to make code readable
var rm = [0,0,0,0]; // resulting matrix
// get the cross product of the x and y axis. It is the area of the rectangle made by the
// two axis
var cross = m[0] * m[3] - m[1] * m[2]; // I call it the cross but most will call
// it the determinate (I think that cross
// product is more suited to geometry while
// determinate is for maths geeks)
rm[0] = m[3] / cross; // invert both axis and unscale (if cross is 1 then nothing)
rm[1] = -m[1] / cross;
rm[2] = -m[2] / cross;
rm[3] = m[0] / cross;
return rm;
}
Now we can invert our matrix
var dimetricMatrixInv = invertMatrix(dimetricMatrix); // get the invers
And now that we have the inverse matrix we can use the transform function to convert from a screen location to a block location
var screen = {x : 100, y : 200};
var blockLoc = transformPoint(screen, dimetricMatrixInv );
// result is the location of the block
The Matrix for rendering
For a bit of magic the transformation matrix dimetricMatrix can also be used by the 2D canvas, but you need to add the origin.
var m = dimetricMatrix;
ctx.setTransform(m[0], m[1], m[2], m[3], 0, 0); // assume origin at 0,0
Now you can draw a box around the block with
ctx.strokeRect(2,1,1,1); // 3rd by 2nd block 1 by 1 block wide.
The origin
I have left out the origin in all the above, I will leave that up to you to find as there is a trillion pages online about matrices as all 2D and 3D rendering use them and getting a good deep knowledge of them is important if you wish to get into computer visualization.
My game has many Laser objects. mx & my represent velocity. I use the following code to draw a line from behind the Laser 2 pixels to ahead of the Laser in the direction it's going 2 pixels.
Removing the first line of the function adjusted the % of the Profiling by ~1% but I don't like the way it looks. I think I could optimize the drawing by sorting by Linewidth but that doesn't appear to get me much.
How else could I optimize this?
Laser.prototype.draw = function(client, context) {
context.lineWidth = Laser.lineWidth;
context.beginPath();
context.moveTo(this.x - this.mx * 2, this.y - this.my * 2);
context.lineTo(this.x + this.mx * 2, this.y + this.my * 2);
context.strokeStyle = this.teamColor;
context.closePath();
context.stroke();
}
Instead of multiplying things by two, why not add them?
E.g.
context.moveTo(this.x - this.mx - this.mx, this.y - this.my - this.my);
context.lineTo(this.x + this.mx + this.mx, this.y + this.my - this.my);
Testing shows that addition is an order of magnitude faster on an imac over multiplication
https://jsfiddle.net/1c85r2pq/
Dont use moveTo or lineTo as they do not use the hardware to render and are very slow. Also your code is drawing the line twice
ctx.beginPath(); // starts a new path
ctx.moveTo(x,y); // sets the start point of a line
ctx.lineTo(xx,yy); // add a line from x,y to xx,yy
// Not needed
ctx.closePath(); // This is not like beginPath
// it is like lineTo and tells the context
// to add a line from the last point xx,yy
// back to the last moveTo which is x,y
This would half the already slow render time.
A quick way to draw lines using bitmaps.
First at the start create an image to hold the bitmap used to draw the line
function createLineSprite(col,width){
var lineSprite = document.createElement("canvas");
var lineSprite.width = 2;
var lineSprite.height = width;
lineSprite.ctx = lineSprite.getContext("2d");
lineSprite.ctx.fillStyle = col;
lineSprite.ctx.fillRect(0,0,2,width);
return lineSprite;
}
var line = createLineSprite("red",4); // create a 4 pixel wide red line sprite
Or you can use an image that you load.
To draw a line you just need to create a transform that points in the direction of the line, and draw that sprite the length of the line.
// draw a line with sprite from x,y,xx,yy
var drawLineSprite = function(sprite,x,y,xx,yy){
var nx = xx-x; // get the vector between the points
var ny = yy-y;
if(nx === 0 && ny === 0){ // nothing to draw
return;
}
var d = Math.hypot(nx,ny); // get the distance. Note IE does not have hypot Edge does
// normalise the vector
nx /= d;
ny /= d;
ctx.setTransform(nx,ny,-ny,nx,x,y); // create the transform with x axis
// along the line and origin at line start x,y
ctx.drawImage(sprite, 0, 0, sprite.width, sprite.height, 0, -sprite.height / 2, d, sprite.height);
}
To draw the line
drawSpriteLine(line,0,0,100,100);
When you are done drawing all the lines you can get the default transform back with
ctx.setTransform(1,0,0,1,0,0);
The sprite can be anything, this allows for very detailed lines and great for game lasers and the like.
If you have many different colours to draw then create one sprite (image) that has many colour on it, then in the line draw function simply draw only the part of the sprite that has the colour you want. You can stretch out a single pixel to any size so you can get many colours on a small bitmap.
I followed a tutorial to create a canvas graph using js. The code plotting is this:
function plotData(context, dataSet, sections, xScale) {
context.lineWidth = 1;
context.outlineWidth = 0;
context.strokeWidth = 0;
context.beginPath();
context.moveTo(0, dataSet[0]);
for (i=0; i<sections; i++) {
context.lineTo(i * xScale, dataSet[i]);
}
context.stroke();
}
I am calling this function with an array holding the points where to go.. The x value is calculated per column (xScale) and the resulting graph if i use more than 1 source of data shows up fine. Screenshot when working fine:
http://s21.postimg.org/vlc1qg9iv/Screen_Shot_2016_04_08_at_15_48_42.png
But when i remove the 2 last data lines and leave only 1 line (so when the graph has a smaller difference between graph max and min values it shows up like this:
http://s16.postimg.org/ex0fakef9/Screen_Shot_2016_04_08_at_15_44_21.png
It is in this screenshot that you can clearly see, that while it should draw a line, the line is not really a 1px line but a shape, much like a (badly) distorted line?
I am not sure if i am doing something wrong or i am plainly ignoring something? The height of the canvas is fixed and it is always calculated using:
canvas = $('#canvas-container canvas')[0];
canvas.width = $('#canvas-container').width() * 0.9;
canvas.height = $('#canvas-container').width() / 1.45;
Thanks!
Codepen of the exact effect (from the exact tutorial) can be found here:
https://codepen.io/anon/pen/JXMwBy?editors=1111
(notice there are 2 more lines of graph data i commented out and in doing so i made the Val_max and Val_min vars different to "stretch" the data in the Y line)
You are stretching the Y axis on every operation after this line:
context.scale(1,-1 * yScale);
Instead, remove the line above and multiply the y values when you draw the line in plotData().
// multiply all Y values by -yScale to flip and scale
context.moveTo(0, dataSet[0] * -yScale);
for (i=1;i<sections;i++) {
context.lineTo(i * xScale, dataSet[i] * -yScale);
}