Creating a rectangle filled with triangles - javascript

I'm doing this in JavaScript ( SVG or Canvas ) but I'm really just looking for pseudocode for how somebody could accomplish this:
If given a rectangle, how can you fill it with various sized, non-overlapping triangles similar to this picture:
http://imgur.com/5XOxpjB
UPDATE
Here is what I came up with for those that are interested. I used D3.js which has a great delaunay function to do this.
var width = 360;
var height = 220;
var vertices = d3.range(100).map(function (d) {
return [Math.random() * width, Math.random() * height];
});
// place coordinates in the corners
[
[0, 0],
[width, 0],
[0, height],
[width, height]
].forEach(function (d) {
vertices.push(d);
});
// add the temporary coordinates that will follow mousemove
vertices.unshift([0, 0]);
var svg = d3.select("#main").append("svg")
.style("width", width + "px")
.style("height", height + "px")
.on("mousemove", function () {
vertices[0] = d3.mouse(this);
draw();
})
.on("click", function () {
vertices.push(d3.mouse(this));
});
var path = svg.append("g").selectAll("path");
draw();
function draw() {
path = path.data(d3.geom.delaunay(vertices).map(function (d) {
return "M" + d.join("L") + "Z";
}), String);
path.exit().remove();
path.enter().append("path").attr("class", function (d, i) {
return "q" + (i % 9) + "-9";
}).attr("d", String);
}
#main {
position: relative;
width: 360px;
height: 220px;
border: 1px solid #ddd;
}
path {
fill: #fff;
stroke: steelblue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="main"></div>

I would do the following:
Define a 2D array for your points, 1D for each line where each entry contains a point for a vertice/corner.
Create points based on a grid
Offset the same points using some form of "jitter". The jitter will only offset the point's position but you will still have the same order in the array
Have a method looping over the arrays drawing a quadrant for each two points on two lines
Split that quadrant into two triangles using the two opposite corner vertices.
Varying jitter should create a similar pattern as shown in the image. To control the jitter you could randomize an angle and radius rather than just the position. Then convert the result of that to a new position.
An ASCII representation (perhaps not useful..):
line 0: xy xy xy xy xy .... vertices for upper part of quadrant
line 1: xy xy xy xy xy .... vertices for lower part and shared
upper part of next quadrant
line 2: xy xy xy xy xy ....
line 3: xy xy xy xy xy ....
...
Apply jitter
Draw in ordered fashion whatever the point position is:
line 0: xy_xy_xy xy xy ....
| /| /| ...
|/_|/_|
line 1: xy xy xy xy xy ....
...
You could also look into Voronoi diagram but be aware of that you'll end up with quadratic polygons and n-gons, the latter can be a bit of a challenge but check Delaunay triangulation for that.

Someone has already written a library to do exactly this. http://qrohlf.com/trianglify/

Related

plotting random points inside a SVG circle so they are not concentrated at the center

I need to plot n number of points inside a circle of say radius R, randomly. When I try to do it with a random theta, and random distance from the center of the circle, points are concentrated towards the center of the circle and rightfully so. Is there a way to have them spread randomly over the circle as shown below in the image?
desired output:
Current output:
below is the code in D3.js for the current output:
while(count){ (count is the number of points to be plotted inside the circle)
var randAngle = Math.random()*Math.PI*2;
var randRadius = Math.random()*radius; (radius is the radius of the circle in which I want to plot)
var randX = cx + randRadius * Math.cos(randAngle);
var randY = cy + randRadius * Math.sin(randAngle);
parent_group_dns.append('rect')
.attr('x', randX)
.attr('y', randY)
.attr('width', 1.5)
.attr('height', 1.5)
.attr('fill', 'white')
count--;
}
is there a way to distribute points not by theta but just by distances?

How to calculate 'start' & 'end' angle of an Arc by given 2 points?

I've been trying to draw an arc on the canvas, using p5.js. I got start & end points, the chord length i calculate using pythagoras using the two points, the height & width values are also given.
In order to draw an arc, i need to use the following function;
arc(x, y, w, h, start, stop, [mode], [detail]) for docs refer to here
The start & stop parameters refer to the start&stop angles specified in radians. I can't draw the arc without those angles and i'm unable to calculate them using what i got.
I searched for lots of examples similar to my question, but it is suggested to calculate the center angle, which i'm also unable to do so. Even though i was able to calculate the center angle, how i'm supposed to get the start&stop angles afterwards?
I have drawn some example illustrations on GeoGebra;
The angle of a vector can be calculated by atan2().
Note, that:
tan(alpha) = sin(alpha) / cos(alpha)
If you've a vector (x, y), then than angle (alpha) of the vector relative to the x-axis is:
alpha = atan2(y, x);
The start_angle and stop_angle of an arc, where the center of the arc is (cpt_x, cpt_y), the start point is (spt_x, spt_y) and the end point is (ept_x, ept_y), can be calculated by:
start_angle = atan2(spt_y-cpt_y, spt_x-cpt_x);
stop_angle = atan2(ept_y-cpt_y, ept_x-cpt_x);
See the example, where the stop angle depends on the mouse position:
var sketch = function( p ) {
p.setup = function() {
let sketchCanvas = p.createCanvas(p.windowWidth, p.windowHeight);
sketchCanvas.parent('p5js_canvas')
}
p.windowResized = function() {
p.resizeCanvas(p.windowWidth, p.windowHeight);
}
p.draw = function() {
let cpt = new p5.Vector(p.width/2, p.height/2);
let rad = p.min(p.width/2, p.height/2) * 0.9;
let stop_angle = p.atan2(p.mouseY-cpt.y, p.mouseX-cpt.x);
p.background(192);
p.stroke(255, 64, 64);
p.strokeWeight(3);
p.noFill();
p.arc(cpt.x, cpt.y, rad*2, rad*2, 0, stop_angle);
}
};
var circle = new p5(sketch);
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.js"></script>
<div id="p5js_canvas"></div>

Generate Gradient Colors for Nodes in Sankey Charts

I'm working with a Sankey chart in javascript (in a react app if that matters), and would like the nodes in Sankey to have a gradient based on their x, y coordinates.
For example, in a diagram like below where x-axis goes to right and y-axis goes down, I'd like to specify a starting color at the origin, and possible a stopping color at max x value and a stopping color at max y value, and have a way to generate a color for each point in the plane given an (x, y) pair.
(origin)
|-------> x coordinate
|
|
v
y coordinate
So is there a gradient formula (get_gradient) that can be used in the Sankey in the following way?
node.append("rect")
.style("fill", function (d) {
return get_gradient(d.x, d.y);
})
Like this?: https://jsfiddle.net/gcbojt2p/1/
The idea is to create 2 color interpolators for x/y. Then, given an x/y pair, convert it to normalized numbers based on the svg width/height. So if x is 500, and the width is 1000, the normalized x is 0.5. Then you can plug that into the interpolater to generate a color. Do the same for the y, and now you have 2 colors to make the gradient.
var y = d3.interpolate('#fff','#2196f3')
var x = d3.interpolate('#fff','#4caf50')
var width = 400;
var height= 400;
d3.select('svg')
.attr('width',width)
.attr('height',height)
.on('mousemove',function(){
var mouse = d3.mouse(this)
var xNormalized = mouse[0]/width
var yNormalized = mouse[1]/height
var xColor = x(xNormalized)
var yColor = y(yNormalized)
var gradient = `linear-gradient(${xColor},${yColor})`
d3.select('div').style('background',gradient)
})

Focusing on geojson in D3.js map

I am trying to view a objects in a topojson file (of buildings in a city) but get the following error:
Error: <path> attribute d: Expected number, "MNaN,NaNLNaN,NaNL…".
Here is my code:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.land {
fill: #e5e5e5;
stroke: #000;
stroke-width: 0.2;
stroke-opacity: 0.8;
}
.states {
fill: none;
stroke: #fff;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/queue.v1.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="http://d3js.org/d3.geo.projection.v0.min.js"></script>
<script>
var width = 800;
var height = 600;
var projection = d3.geo.mercator()
.center([30, 30])
.scale(500)
.translate([width / 2, height / 2]);
var path = d3.geo.path().projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
queue()
.defer(d3.json, "cairomonuments.json")
.await(ready);
function ready(error, cairo) {
if (error) throw error;
// Refine projection
var b, s, t;
projection.scale(1).translate([0, 0]);
var b = path.bounds(cairo);
var s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height);
var t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
projection.scale(s).translate(t);
svg.selectAll("path")
.data(topojson.feature(cairo, cairo.objects.monuments).features)
.enter()
.append('path')
.attr('class', 'land')
.attr('d', path);
}
</script>
</body>
I just want to center the map on my geojson file and flip it sideways. What am I missing?
topojson file here
The problem
The primary issue as far as I can see is this line:
var b = path.bounds(cairo);
path.bounds won't produce expected results with a collection of features (such as your layer). Instead it:
Computes the projected bounding box (in pixels) for the specified
feature. The bounding box is represented by a two-dimensional array:
[[left, top], [right, bottom]] , different from GIS geo.bounds'
convention.
Also, you aren't passing it geojson, you're passing it topojson. If you wanted to use a bounds of a specific feature, your code would look more like:
var b = path.bounds(topojson.feature(cairo, cairo.objects.monuments).features[0]);
Even if you pass it a singular feature in the right format, it still won't project correctly as your scale was defined as 500 earlier when you defined the projection - this will warp the calculations when dynamically re-calculating the scale.
Possible Solution (Keeping d3.js v3)
Topojson generally has a bbox property. You could use this to get your centering coordinate:
var x = (cairo.bbox[0] + cairo.bbox[2]) / 2; // get middle x coordinate
var y = (cairo.bbox[1] + cairo.bbox[3]) / 2; // get middle y coordinate
Note that the order of a geojson or topojson bounding box is : left, bottom, right, top.
So we can easily center the map on the layer center now:
projection.center([x,y]) or projection.rotate([-x,0]).center([0,y]) or projection.rotate([-x,-y]).
Now all that is left is to calculate the scale (set it at one to start).
If path.bounds returns a two coordinate array of the top left and bottom right coordinates ([min x, min y],[max x, max y], in SVG coordinate space), then we can produce an equivalent array using the topojson.bbox:
var b = [ projection([cairo.bbox[0],cairo.bbox[3]]),projection([cairo.bbox[2],cairo.bbox[1]]) ];
Here it's a little tricky as the SVG coordinate space has y coordinates starting from zero at the top (reversed from the geographic features), and the order of coordinates in the bounds is: left top right bottom (again, different than geographic features).
That leaves us with the calculation you already had:
var s = 0.95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height);
Which altogether gives us:
Initial declaration of scale:
var projection = d3.geo.mercator()
.scale(1)
.translate([width / 2, height / 2]);
Refinement of scale and center based on data layer:
var x = (cairo.bbox[0] + cairo.bbox[2]) / 2;
var y = (cairo.bbox[1] + cairo.bbox[3]) / 2;
projection.rotate([-x,-y]);
var b = [ projection([cairo.bbox[0],cairo.bbox[3]]),projection([cairo.bbox[2],cairo.bbox[1]]) ];
var s = 0.95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height);
projection.scale(s);
Here's a bl.ock demonstrating it all in action.
Flipping the map
There is a seldom used parameter in the projection rotation that allows you to achieve this. In my bl.ock above and in the code block above I used rotate to center the map projection. By adding a third parameter I can rotate the map relative to the viewport:
projection.rotate([-x,-y,90]);

D3 put arc labels in a Pie Chart if there is enough space

I will put a text element in every arc of my Pie Chart (center) - as shown in this example:
http://bl.ocks.org/mbostock/3887235
But I will only put the text element if the room is sufficient for the whole text, so im must compare the size of my text element with the "available" space in every arc.
I think I can do this with getBBox() to get the text dimensions... but how can I get (and compare) the dimension of the available space in every arc.
thx...!
This question has been asked several times before.
The solutions I have suggested there is to rotate the label but it has never quite satisfied me. Part of it was the horrible font rendering done by some browsers and loss in legibility that brings and the weird flip when one label crosses over the 180° line. In some cases, the results were acceptable and unavoidable, e.g. when the labels were too long.
One of the other solution, the one suggested by Lars, is to put the labels outside the pie chart. However, that just pushes the labels outside, granting them a larger radius, but does not solve the overlap problem completely.
The other solution is actually using the technique you suggest: just remove the labels which do not fit.
Hide overflowing labels
Compare Original, which has >= 65 label overflowing to Solution where the overflowing label is gone.
Reducing the problem
The key insight is to see that this problem is of finding whether one convex polygon (a rectangle, the bounding box) is contained inside another convex polygon(-ish) (a wedge).
The problem can be reduced to finding whether all the points of the rectangle lie inside the wedge or not. If they do, then the rectangle lies inside the arc.
Does a point lie inside a wedge
Now that part is easy. All one needs to do is to check:
The distance of the point from the center is less than the radius
The angle subtended by the point on the center is between the startAngle and endAngle of the arc.
function pointIsInArc(pt, ptData, d3Arc) {
// Center of the arc is assumed to be 0,0
// (pt.x, pt.y) are assumed to be relative to the center
var r1 = d3Arc.innerRadius()(ptData), // Note: Using the innerRadius
r2 = d3Arc.outerRadius()(ptData),
theta1 = d3Arc.startAngle()(ptData),
theta2 = d3Arc.endAngle()(ptData);
var dist = pt.x * pt.x + pt.y * pt.y,
angle = Math.atan2(pt.x, -pt.y); // Note: different coordinate system.
angle = (angle < 0) ? (angle + Math.PI * 2) : angle;
return (r1 * r1 <= dist) && (dist <= r2 * r2) &&
(theta1 <= angle) && (angle <= theta2);
}
Find the bounding box of the labels
Now that we have that out of the way, the second part is figuring out what are the four corners of the rectangle. That, also, is easy:
g.append("text")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.age; })
.each(function (d) {
var bb = this.getBBox(),
center = arc.centroid(d);
var topLeft = {
x : center[0] + bb.x,
y : center[1] + bb.y
};
var topRight = {
x : topLeft.x + bb.width,
y : topLeft.y
};
var bottomLeft = {
x : topLeft.x,
y : topLeft.y + bb.height
};
var bottomRight = {
x : topLeft.x + bb.width,
y : topLeft.y + bb.height
};
d.visible = pointIsInArc(topLeft, d, arc) &&
pointIsInArc(topRight, d, arc) &&
pointIsInArc(bottomLeft, d, arc) &&
pointIsInArc(bottomRight, d, arc);
})
.style('display', function (d) { return d.visible ? null : "none"; });
The pith of the solution is in the each function. We first place the text at the right place so that the DOM renders it. Then we use the getBBox() method to get the bounding box of the text in the user space. A new user space is created by any element which has a transform attribute set on it. That element, in our case, is the text box itself. So the bounding box returned is relative to the center of the text, as we have set the text-anchor to be middle.
The position of the text relative to the arc can be calculated since we have applied the transformation 'translate(' + arc.centroid(d) + ')' to it. Once we have the center, we just calculate the topLeft, topRight, bottomLeft and bottomRight points from it and see whether they all lie inside the wedge.
Finally, we determine if all the points lie inside the wedge and if they do not fit, set the display CSS property to none.
Working demo
Original
Solution
Note
I am using the innerRadius which, if non zero, makes the wedge non-convex which will make the calculations much more complex! However, I think the danger here is not significant since the only case it might fail is this, and, frankly, I don't think it'll happen often (I had trouble finding this counter example):
x and y are flipped and y has a negative sign while calculating Math.atan2. This is because of the difference between how Math.atan2 and d3.svg.arc view the coordinate system and the direction of positive y with svg.
Coordinate system for Math.atan2
θ = Math.atan2(y, x) = Math.atan2(-svg.y, x)
Coordinate system for d3.svg.arc
θ = Math.atan2(x, y) = Math.atan2(x, -svg.y)
You can't really do this with the bounding box because the bounding box is much larger than a wedge for the pie chart wedges. That is, even though the wedge at the outer edge would be wide enough to accommodate the text, that doesn't mean that it's wide enough at the actual position of the text.
Unfortunately, there's no easy way of doing what you're trying to do (pixel-level overlap testing). See e.g. this question for some more information. I would suggest simply putting the text labels outside of the pie chart so you don't run into this problem.

Categories

Resources