How to cluster bubble chart with many groups - javascript

I'm trying to imitate the following effects:
Orginal version V3:
https://bl.ocks.org/mbostock/7881887
Converted to V4:
https://bl.ocks.org/lydiawawa/1fe3c80d35e046c1636663442f34680b/86d1bda1dabb7f3a6d11cb1a16053564078ed964
An example used dataset:
https://jsfiddle.net/hf998do7/1/
This is what I have so far :
https://blockbuilder.org/lydiawawa/0899a02cc86f2274f52e27064bc86500
I want to make a bubble graph that shows clusters of Race, the size of the bubbles are assigned by BMI. The dots will not overlap (collide). There is a toggle control on the left top corner; when it is turned to the right, bubbles cluster into groups separated by Race, when it is turned to the left the dots combine and mix together and centers on the canvas ( in a large circle).
I tried to code for a clustered bubbles, but with the dataset I have there are too many jitters, takes a while for the split to stop. So I found Bostock's versions of clustered bubbles, they are intended to reduce jitters.
What I struggled the most is to fit my dataset with Bostock's version III layout, and I accidentally used the wrong version of forceCluster(alpha) code because I was looking through his different versions (I updated the code in the following section).
In Bostock's original code, he created his own bubbles by defining the following variable, nodes. He used a custom formula to define the distribution of nodes. However, in my case, I will be using my own dataset, bubbleChart.csv with a toggle that combines and splits the cluster.
Bostock's custom defined nodes:
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
r = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
d = {
cluster: i,
radius: r,
x: Math.cos(i / m * 2 * Math.PI) * 200 + width / 2 + Math.random(),
y: Math.sin(i / m * 2 * Math.PI) * 200 + height / 2 + Math.random()
};
if (!clusters[i] || (r > clusters[i].radius)) clusters[i] = d;
return d;
});
He then defined force function to reduce jitter:
var force = d3.layout.force()
.nodes(nodes) // not sure how to redefine this
.size([width, height])
.gravity(.02)
.charge(0)
.on("tick", tick)
.start();
Code for clustering (updated):
function forceCluster(alpha) {
return function(d) {
var cluster = clusters[d.Race];
if (cluster === d) return;
var x = d.x - cluster.x,
y = d.y - cluster.y,
l = Math.sqrt(x * x + y * y),
r = d.Race + cluster.Race;
if (l != r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
};
}
I'm having trouble to fit everything together in particularly to adapt to my own dataset and would like to have some help. Thank you!

Related

Draw Map in Browser out of 2 Dimensional Array of Distances

I'm receiving all distances between a random number of points in a 2 dimensional coordinate system.
How can I visualize this as coordinates on a map in my browser?
In case there are many solutions I just want to see the first possible one that my algorithm can come up with.
So here's an extremely easy example:
PointCount = 3
Distances:
0-1 = 2
0-2 = 4
1-2 = 2
Does anyone know an easy way (existing solution/framework maybe) to do it using whatever is out there to make it easier to implement?
I was thinking maybe using the html canvas element for drawing, but I don't know how to create an algorithm that could come up with possible coordinates for those points.
The above example is simplified -
Real distance values could look like this:
(0) (1) (2) (3)
(0) 0 2344 3333 10000
(1) 0 3566 10333
(2) 0 12520
I'm not sure this is relevant for SO, but anyway...
The way to do this is quite simply to place the points one by one using the data:
Pick a random location for the first point (let's say it's 0,0).
The second point is on a circle with radius d(0,1) with the first point as its center, so you can pick any point on the circle. Let's pick (d(0,1),0).
The third point is at the intersection of a circle with radius d(0,2) and center point 1, and a circle with radius d(1,2) and center point 2. You will get either 0, 1, 2 or an infinity of solutions. If the data comes from real points, 0 shouldn't happen. 1 and infinity are edge cases, but you should still handle them. Pick any of the solutions.
The fourth point is at the intersection of 3 circles. Unless you're very unlucky (but you should account for it), there should be only one solution.
Continue like this until all points have been placed.
Note that this doesn't mean you'll get the exact locations of the original points: you can have any combination of a translation (the choice of your first point), rotation (the choice of your second point) and symmetry (the choice of your third point) making the difference.
A quick and dirty implementation (not handling quite a few cases, and tested very little):
function distance(p1, p2) {
return Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2));
}
// adapted from https://stackoverflow.com/a/12221389/3527940
function intersection(x0, y0, r0, x1, y1, r1) {
var a, dx, dy, d, h, rx, ry;
var x2, y2;
/* dx and dy are the vertical and horizontal distances between
* the circle centers.
*/
dx = x1 - x0;
dy = y1 - y0;
/* Determine the straight-line distance between the centers. */
d = Math.sqrt((dy * dy) + (dx * dx));
/* Check for solvability. */
if (d > (r0 + r1)) {
/* no solution. circles do not intersect. */
return false;
}
if (d < Math.abs(r0 - r1)) {
/* no solution. one circle is contained in the other */
return false;
}
/* 'point 2' is the point where the line through the circle
* intersection points crosses the line between the circle
* centers.
*/
/* Determine the distance from point 0 to point 2. */
a = ((r0 * r0) - (r1 * r1) + (d * d)) / (2.0 * d);
/* Determine the coordinates of point 2. */
x2 = x0 + (dx * a / d);
y2 = y0 + (dy * a / d);
/* Determine the distance from point 2 to either of the
* intersection points.
*/
h = Math.sqrt((r0 * r0) - (a * a));
/* Now determine the offsets of the intersection points from
* point 2.
*/
rx = -dy * (h / d);
ry = dx * (h / d);
/* Determine the absolute intersection points. */
var xi = x2 + rx;
var xi_prime = x2 - rx;
var yi = y2 + ry;
var yi_prime = y2 - ry;
return [
[xi, yi],
[xi_prime, yi_prime]
];
}
function generateData(nbPoints) {
var i, j, k;
var originalPoints = [];
for (i = 0; i < nbPoints; i++) {
originalPoints.push([Math.random() * 20000 - 10000, Math.random() * 20000 - 10000]);
}
var data = [];
var distances;
for (i = 0; i < nbPoints; i++) {
distances = [];
for (j = 0; j < i; j++) {
distances.push(distance(originalPoints[i], originalPoints[j]));
}
data.push(distances);
}
//console.log("original points", originalPoints);
//console.log("distance data", data);
return data;
}
function findPointsForDistances(data, threshold) {
var points = [];
var solutions;
var solutions1, solutions2;
var point;
var i, j, k;
if (!threshold)
threshold = 0.01;
// First point, arbitrarily set at 0,0
points.push([0, 0]);
// Second point, arbitrarily set at d(0,1),0
points.push([data[1][0], 0]);
// Third point, intersection of two circles, pick any solution
solutions = intersection(
points[0][0], points[0][1], data[2][0],
points[1][0], points[1][1], data[2][1]);
//console.log("possible solutions for point 3", solutions);
points.push(solutions[0]);
//console.log("solution for points 1, 2 and 3", points);
found = true;
// Subsequent points, intersections of n-1 circles, use first two to find 2 solutions,
// the 3rd to pick one of the two
// then use others to check it's valid
for (i = 3; i < data.length; i++) {
// distances to points 1 and 2 give two circles and two possible solutions
solutions = intersection(
points[0][0], points[0][1], data[i][0],
points[1][0], points[1][1], data[i][1]);
//console.log("possible solutions for point " + (i + 1), solutions);
// try to find which solution is compatible with distance to point 3
found = false;
for (j = 0; j < 2; j++) {
if (Math.abs(distance(solutions[j], points[2]) - data[i][2]) <= threshold) {
point = solutions[j];
found = true;
break;
}
}
if (!found) {
console.log("could not find solution for point " + (i + 1));
console.log("distance data", data);
console.log("solution for points 1, 2 and 3", points);
console.log("possible solutions for point " + (i + 1), solutions);
console.log("distances to point 3",
distance(solutions[0], points[2]),
distance(solutions[1], points[2]),
data[i][2]
);
break;
}
// We have found a solution, we need to check it's valid
for (j = 3; j < i; j++) {
if (Math.abs(distance(point, points[j]) - data[i][j]) > threshold) {
console.log("Could not verify solution", point, "for point " + (i + 1) + " against distance to point " + (j + 1));
found = false;
break;
}
}
if (!found) {
console.log("stopping");
break;
}
points.push(point);
}
if (found) {
//console.log("complete solution", points);
return points;
}
}
console.log(findPointsForDistances([
[],
[2344],
[3333, 3566],
[10000, 10333, 12520],
]));
console.log(findPointsForDistances([
[],
[2],
[4, 2],
]));
console.log(findPointsForDistances([
[],
[4000],
[5000, 3000],
[3000, 5000, 4000]
]));
console.log(findPointsForDistances([
[],
[2928],
[4938, 3437],
[10557, 10726, 13535]
]));
var nbPoints, i;
for (nbPoints = 4; nbPoints < 8; nbPoints++) {
for (i = 0; i < 10; i++) {
console.log(findPointsForDistances(generateData(nbPoints)));
}
}
Fiddle here: https://jsfiddle.net/jacquesc/82aqmpnb/15/
Minimum working example. Remember that in canvas coordinates, the y value is inverted but you could do something like:
y = canvasHeight - y
If you also have negative points then if would take a little bit of extra work. Also it may be helpful in that case to draw lines and tick marks to visualize the axis.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
let scale = 10;
let radius = 10;
function point(x, y) {
ctx.fillRect(x*scale, y*scale, radius, radius);
}
// test
point(10, 15);
point(20, 8);
<html>
<body>
<canvas id="canvas" width=1000 height=1000></canvas>
</body>
</html>
There are plenty of libraries out there.
chartist.js is easy to use and responsive JavaS cript library. I used it last year for basic charts after trying many others but it was the only one that scaling easily in different screen sizes.
chartJS is another better looking library.
And you can use html5 canvas it's easy and fun but it will take time especially in scaling.
To scale and position, you should use the minimum and maximum values for x and y.
Good luck

Adding points to Voronoi Diagram in d3

I am using d3 to create a diagram to try and speed up a nearest nabour search within a function that plots points on a plane.
Is there a way to add points directly to the diagram so I can add the points within a while loop instead of re-drawing the entire voronoi?
var svg = d3.select("svg")
var distance = function(pa, pb) {
var x = pa[0] - pb[0],
y = pa[1] - pb[1]
return Math.sqrt((x * x) + (y * y))
}
var scatterCircle = function(point, radius, quantity, proximity, margin) {
var x1 = point[0] - radius,
y1 = point[1] - radius,
inner = radius * margin,
array = [
[500, 500]
]
//should be declaring diagram here and addings points below//
while (array.length < quantity) {
//constructing new diagram each loop to test function, needs add to diagram function//
var newpoly = d3.voronoi()(array),
x = x1 + (radius * 2 * Math.random()),
y = y1 + (radius * 2 * Math.random()),
ii = newpoly.find(x, y).index
var d = distance(array[ii], [x, y]),
e = distance([x, y], point)
if (e < inner) {
if (d > proximity) {
array.push([x, y])
}
}
}
return array
}
var test = scatterCircle([500, 500], 500, 1500, 10, 0.9)
var o = 0
while (o < test.length) {
svg.append("circle")
.attr("cx", test[o][0])
.attr("cy", test[o][1])
.attr("r", 1)
o++
}
<script src="https://d3js.org/d3.v4.js"></script>
<svg width="1000" height="1000">
I am no expert in d3.js but I will share what I found out. The implemented algorithm for Voronoi diagrams is Fortune's algorithm. This is the classical algorithm to compute a Voronoi diagram. Inserting a new point is neither part of this algorithm nor of the function set documented for d3.js. But you are correct, inserting one new site does not require to redraw the whole diagram in theory.
You use the Voronoi diagram for NNS (nearest neighbour search). You could also use a 2d-tree to accomplish NNS. There insertion and removal is easier. A quick search revealed two implementations in javascript: kd-tree-javascript and kd-tree-js.

D3 Force Layout - Text Wrapping and Node Overlaps

This is my first question to stack overflow so please bear with me if I make some newbie mistakes. I have searched a lot of questions here and have not found exactly what I'm looking for (in one case I have, but do not know how to implement it). And it seems the only people who have asked similar questions have not received any answers.
I've created a force layout with D3 and things are almost working the way I want them to. Two things that I am having trouble editing for:
1) Keep nodes from overlapping: yes, I have read and re-read Mike Bostock's code for clustered force layouts. I do not know how to implement this into my code without something going terribly wrong! I tried this code from a tutorial, but it fixed my nodes in a corner and splayed the links all over the canvas:
var padding = 1, // separation between circles
radius=8;
function collide(alpha) {
var quadtree = d3.geom.quadtree(graph.nodes);
return function(d) {
var rb = 2*radius + padding,
nx1 = d.x - rb,
nx2 = d.x + rb,
ny1 = d.y - rb,
ny2 = d.y + rb;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y);
if (l < rb) {
l = (l - rb) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
You can see the addition to the tick function in my fiddle (linked below) commented out.
2) Wrap my text labels so they fit inside the nodes. Right now they expand to the node's full name over hover but I am going to change that into a tooltip eventually (once I get these kinks worked out I'll figure out a tooltip) - right now I just want the original, short names to wrap inside the nodes. I've looked at this answer and this answer (http://jsfiddle.net/Tmj7g/4/) but when I try to implement this into my own code, it is not responding or ends up clustering all the nodes in the top left corner (??).
Any and all input is GREATLY appreciated, and feel free to edit my fiddle here: https://jsfiddle.net/lilyelle/496c2bmr/
I also know that all of my language is not entirely consistent or the simplest way of writing the D3 code - that's because I've copied and spliced together lots of things from different sources and am still trying to figure out the best way to write this stuff for myself. Any advice in this regard is also appreciated.
1) Collision detection: Here's an updated, working jsFiddle, which was guided by this example from mbostock. Adding collision detection was largely a copy/paste of the important bits. Specifically, in the tick function, I added the code that loops over all those nodes and tweaks their positions if they collide:
var q = d3.geom.quadtree(nodes),
i = 0,
n = nodes.length;
while (++i < n) q.visit(collide(nodes[i]));
Since your jsFiddle didn't have a variable nodes set, I added it right above the last snipped:
var nodes = force.nodes()
Also, that loop requires the function collide to be defined, just as it is in Bostock's example, so I added that to your jsFiddle as well:
function collide(node) {
var r = node.radius + 16,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.radius + quad.point.radius;
if (l < r) {
l = (l - r) / l * .5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
The last necessary bit comes from the fact that detecting node collision requires knowing their size. Bostock's code accesses node.radius inside the collide function above. Your example doesn't set the nodes' radii, so in that collide function node.radius is undefined. One way to set this radius is to add it to your json, e.g.
{radius: 30, "name":"AATF", "full_name":"African Agricultural Technology Foundation", "type":1}
If all your nodes will have the same radius, that's overkill.
Another way is to replace the 2 occurrences of node.radius with a hardcoded number, like 30.
I chose to do something between those two options: assign a constant node.radius to each node by looping over the loaded json:
json.nodes.forEach(function(node) {
node.radius = 30;
})
That's it for getting collision detection working. I used a radius of 30 because that's the radius you used for rendering these nodes, as in .attr("r", 30). That'll keep all the nodes bunched up — not overlapping but still touching each other. You can experiment with a larger value for node.radius to get some white space between them.
2) Text Wrapping: That's a tough one. There's no easy way to make the SVG <text> wrap at some width. Only regular html div/span can do that automatically, but even html elements can't wrap to fit a circle, only to a constant width.
You might be able to come up with some compromise that'll allow you to always fit some text. For example, if your data is all known ahead of time and the size of the circles is always the same fixed value, then you know ahead of time which labels can fit and which don't. The ones that don't, you can either shorten, by adding say a short_name attribute to each node in your JSON, and setting it to something that'll definitely fit. Alternatively, still if the size and labels are known in advance, you can pre-determine how to break up the labels into multiple lines and hard code that into your JSON. Then, when you render, you can render that text using multiple SVG <text> elements that you manually position as multiple lines. Finally, if nothing is known ahead of time, then you might be able to get a good solution by switching to rendering the text as absolutely positioned divs on top of (and outside of) the SVG, with a widths that match the circles' widths, so that the text would automatically wrap.

How is data parsed in this 3D piechart?

I'm trying to grasp how the functions in Donut3D.js -> http://plnkr.co/edit/g5kgAPCHMlFWKjljUc3j?p=preview handle the inserted data:
Above all, where is it set that the data's startAngle is set at 0 degrees?
I want to change it to 45º, then to 135º, 225º and 315º (look at the image above).
I've located this function:
Donut3D.draw = function(id, data, x /*center x*/, y/*center y*/,
rx/*radius x*/, ry/*radius y*/, h/*height*/, ir/*inner radius*/){
var _data = d3.layout.pie().sort(null).value(function(d) {return d.value;})(data);
var slices = d3.select("#"+id).append("g").attr("transform", "translate(" + x + "," + y + ")")
.attr("class", "slices");
slices.selectAll(".innerSlice").data(_data).enter().append("path").attr("class", "innerSlice")
.style("fill", function(d) {
return d3.hsl(d.data.color).darker(0.7); })
.attr("d",function(d){
return pieInner(d, rx+0.5,ry+0.5, h, ir);})
.each(function(d){this._current=d;});
slices.selectAll(".topSlice").data(_data).enter().append("path").attr("class", "topSlice")
.style("fill", function(d) {
return d.data.color; })
.style("stroke", function(d) {
return d.data.color; })
.attr("d",function(d){
return pieTop(d, rx, ry, ir);})
.each(function(d){this._current=d;});
slices.selectAll(".outerSlice").data(_data).enter().append("path").attr("class", "outerSlice")
.style("fill", function(d) {
return d3.hsl(d.data.color).darker(0.7); })
.attr("d",function(d){
return pieOuter(d, rx-.5,ry-.5, h);})
.each(function(d){this._current=d;});
slices.selectAll(".percent").data(_data).enter().append("text").attr("class", "percent")
.attr("x",function(d){
return 0.6*rx*Math.cos(0.5*(d.startAngle+d.endAngle));})
.attr("y",function(d){
return 0.6*ry*Math.sin(0.5*(d.startAngle+d.endAngle));})
.text(getPercent).each(function(d){this._current=d;});
}
and tried to insert an arc such as :
var arc = d3.svg.arc().outerRadius(r)
.startAngle(function(d) { return d.startAngle + Math.PI/2; })
.endAngle(function(d) { return d.endAngle + Math.PI/2; });
but it doesn't produce the desired effects.
EDIT 1
The first answer helped in rotating the inner pie, by changing:
var _data = d3.layout.pie().sort(null).value(function(d) {
return d.value;
})(data);
to
var _data = d3.layout.pie()
.startAngle(45*Math.PI/180)
.endAngle(405*Math.PI/180).sort(null).value(function(d) {
return d.value;
})(data);
the problem is that now the outer pie gets broken -> http://plnkr.co/edit/g5kgAPCHMlFWKjljUc3j?p=preview
I guess the solution has something to do with the function function pieOuter(d, rx, ry, h ) and the two startAngle and endAngle variables, but they work in apparently unpredictable ways.
Thank you
I know that Pie Charts are bad, especially if in 3D; but this work
is part of my thesis where my job is actually demonstrate how
PieCharts are Bad! I want to rotate this PieChart in order to show how
if the 3D pie Slice is positioned at the top the data shows as less
important, or more important if positioned at the bottom. So a 'Evil
Journalist' could alter the visual perception of data by simply
inclinating and rotating the PieChart!
Here's a corrected function which allows rotation.
First, modify function signature to include rotate variable:
Donut3D.draw = function(id, data, x /*center x*/ , y /*center y*/ ,
rx /*radius x*/ , ry /*radius y*/ , h /*height*/ , ir /*inner radius*/, rotate /* start angle for first slice IN DEGREES */ ) {
In the draw function, modify angles. Instead of screwing with pie angles, I'd do it to the data directly:
_data.forEach(function(d,i){
d.startAngle += rotate * Math.PI/180; //<-- convert to radians
d.endAngle += rotate * Math.PI/180;
});
Then you need to correct the pieOuter function to fix the drawing artifacts:
function pieOuter(d, rx, ry, h) {
var startAngle = d.startAngle,
endAngle = d.endAngle;
var sx = rx * Math.cos(startAngle),
sy = ry * Math.sin(startAngle),
ex = rx * Math.cos(endAngle),
ey = ry * Math.sin(endAngle);
// both the start and end y values are above
// the middle of the pie, don't bother drawing anything
if (ey < 0 && sy < 0)
return "M0,0";
// the end is above the pie, fix the points
if (ey < 0){
ey = 0;
ex = -rx;
}
// the beginning is above the pie, fix the points.
if (sy < 0){
sy = 0;
sx = rx;
}
var ret = [];
ret.push("M", sx, h + sy, "A", rx, ry, "0 0 1", ex, h + ey, "L", ex, ey, "A", rx, ry, "0 0 0", sx, sy, "z");
return ret.join(" ");
}
Here's the full code
Changing the default start angle
Donut3D users d3's pie layout function here, which has a default startAngle of 0.
If you want to change the start angle, you should modify donut3d.js.
In the first place, you should certainly avoid to use 3d pie/donut charts, if you care about usability and readability of your visualizations - explained here.
Fixing bottom corner layout
The endAngle you are using is not correct, causing the "light blue" slice to overlap the "blue" one. Should be 405 (i.e. 45 + 360) instead of 415.
var _data = d3.layout.pie()
.startAngle(45*Math.PI/180)
.endAngle(405*Math.PI/180)
Then, the "pieOuter" angles calculation should be updated to behave correctly. The arc which doesn't work is the one where endAngle > 2 * PI, and the angle computation should be updated for it.
This does the trick (don't ask me why):
// fix right-side outer shape
if (d.endAngle > 2 * Math.PI) {
startAngle = Math.PI / 120
endAngle = Math.PI/4
}
demo: http://plnkr.co/edit/wmPnS9XVyQcrNu4WLa0D?p=preview

Find column, row on 2D isometric grid from x,y screen space coords (Convert equation to function)

I'm trying to find the row, column in a 2d isometric grid of a screen space point (x, y)
Now I pretty much know what I need to do which is find the length of the vectors in red in the pictures above and then compare it to the length of the vector that represent the bounds of the grid (which is represented by the black vectors)
Now I asked for help over at mathematics stack exchange to get the equation for figuring out what the parallel vectors are of a point x,y compared to the black boundary vectors. Link here Length of Perpendicular/Parallel Vectors
but im having trouble converting this to a function
Ideally i need enough of a function to get the length of both red vectors from three sets of points, the x,y of the end of the 2 black vectors and the point at the end of the red vectors.
Any language is fine but ideally javascript
What you need is a base transformation:
Suppose the coordinates of the first black vector are (x1, x2) and the coordinates of the second vector are (y1, y2).
Therefore, finding the red vectors that get at a point (z1, z2) is equivalent to solving the following linear system:
x1*r1 + y1*r2 = z1
x2*r1 + y2*r2 = z2
or in matrix form:
A x = b
/x1 y1\ |r1| = |z1|
\x2 y2/ |r2| |z2|
x = inverse(A)*b
For example, lets have the black vector be (2, 1) and (2, -1). The corresponding matrix A will be
2 2
1 -1
and its inverse will be
1/4 1/2
1/4 -1/2
So a point (x, y) in the original coordinates will be able to be represened in the alternate base, bia the following formula:
(x, y) = (1/4 * x + 1/2 * y)*(2,1) + (1/4 * x -1/2 * y)*(2, -1)
What exactly is the point of doing it like this? Any isometric grid you display usually contains cells of equal size, so you can skip all the vector math and simply do something like:
var xStep = 50,
yStep = 30, // roughly matches your image
pointX = 2*xStep,
pointY = 0;
Basically the points on any isometric grid fall onto the intersections of a non-isometric grid. Isometric grid controller:
screenPositionToIsoXY : function(o, w, h){
var sX = ((((o.x - this.canvas.xPosition) - this.screenOffsetX) / this.unitWidth ) * 2) >> 0,
sY = ((((o.y - this.canvas.yPosition) - this.screenOffsetY) / this.unitHeight) * 2) >> 0,
isoX = ((sX + sY - this.cols) / 2) >> 0,
isoY = (((-1 + this.cols) - (sX - sY)) / 2) >> 0;
// isoX = ((sX + sY) / isoGrid.width) - 1
// isoY = ((-2 + isoGrid.width) - sX - sY) / 2
return $.extend(o, {
isoX : Math.constrain(isoX, 0, this.cols - (w||0)),
isoY : Math.constrain(isoY, 0, this.rows - (h||0))
});
},
// ...
isoToUnitGrid : function(isoX, isoY){
var offset = this.grid.offset(),
isoX = $.uD(isoX) ? this.isoX : isoX,
isoY = $.uD(isoY) ? this.isoY : isoY;
return {
x : (offset.x + (this.grid.unitWidth / 2) * (this.grid.rows - this.isoWidth + isoX - isoY)) >> 0,
y : (offset.y + (this.grid.unitHeight / 2) * (isoX + isoY)) >> 0
};
},
Okay so with the help of other answers (sorry guys neither quite provided the answer i was after)
I present my function for finding the grid position on an iso 2d grid using a world x,y coordinate where the world x,y is an offset screen space coord.
WorldPosToGridPos: function(iPosX, iPosY){
var d = (this.mcBoundaryVectors.upper.x * this.mcBoundaryVectors.lower.y) - (this.mcBoundaryVectors.upper.y * this.mcBoundaryVectors.lower.x);
var a = ((iPosX * this.mcBoundaryVectors.lower.y) - (this.mcBoundaryVectors.lower.x * iPosY)) / d;
var b = ((this.mcBoundaryVectors.upper.x * iPosY) - (iPosX * this.mcBoundaryVectors.upper.y)) / d;
var cParaUpperVec = new Vector2(a * this.mcBoundaryVectors.upper.x, a * this.mcBoundaryVectors.upper.y);
var cParaLowerVec = new Vector2(b * this.mcBoundaryVectors.lower.x, b * this.mcBoundaryVectors.lower.y);
var iGridWidth = 40;
var iGridHeight = 40;
var iGridX = Math.floor((cParaLowerVec.length() / this.mcBoundaryVectors.lower.length()) * iGridWidth);
var iGridY = Math.floor((cParaUpperVec.length() / this.mcBoundaryVectors.upper.length()) * iGridHeight);
return {gridX: iGridX, gridY: iGridY};
},
The first line is best done once in an init function or similar to save doing the same calculation over and over, I just included it for completeness.
The mcBoundaryVectors are two vectors defining the outer limits of the x and y axis of the isometric grid (The black vectors shown in the picture above).
Hope this helps anyone else in the future

Categories