In effort to learn D3.js, I took this example and made a plunker out of it
http://plnkr.co/edit/ynWB0GznvrMMRmqkaqaA?p=preview
Problem?
The data doesn't fit correctly and you can see that data is incomplete
How shall I fix it?
First thing is that your labels are incorrect. So change this:
.text(function(d) {
return 'd.data.age'; });
to this:
.text(function(d) {
return d.data.age; });
and you'll get the correct labels.
Second, the labels are getting cut off. This appears to be to do with the sizes specified for the pie chart. You can either increase the width specified (looks fine at 360 say):
var width = 360,
height = 300,
radius = Math.min(width, height) / 2;
Or bring the labels in closer to the graph:
.attr("transform", function(d) {
return "translate(" + ( (radius -50) * Math.sin( ((d.endAngle - d.startAngle) / 2) + d.startAngle ) ) + "," + ( -1 * (radius - 50) * Math.cos( ((d.endAngle - d.startAngle) / 2) + d.startAngle ) ) + ")"; })
Where it says radius - x determines how close or far the labels will appear to the graph. The bigger x is, the closer they will be.
Related
I have a D3js map built with topojson.js.
var projection = d3.geo.mercator();
Everything works fine, but I am looking for an effect that I cannot manage to achieve. When, zooming the map I would like the pins over it to scale down, But if I scale it down, I need to recalculate their coordinates and I can't find the formula to do so.
Zoom handler
scale = d3.event.scale;
if (scale >= 1) {
main.attr("transform", "translate(" + d3.event.translate + ")
scale(" + d3.event.scale + ")");
}
else {
main.attr("transform", "translate(" + d3.event.translate + ")scale(1)");
}
//43x49 is the size initial pine size when scale = 1
var pins = main.select("#pins").selectAll("g").select("image")
.attr("width", function () {
return 43 - (43 * (scale - 1));
})
.attr("height", function () {
return 49 - (49 * (scale - 1));
})
.attr("x", function () {
//Calculate new image coordinates;
})
.attr("y", function () {
//Calculate new image coordinates;
});
My question is : how do I calculate x and y based on new scale?
I hope I am clear enough.
Thanks for your help
EDIT :
Calculation of initial pins coordinates :
"translate(" + (projection([d.lon, d.lat])[0] - 20) + ","
+ (projection([d.lon, d.lat])[1] - 45) + ")"
-20 and -45 to have the tip of the pin right on the target.
You need to "counter-scale" the pins, i.e. as main scales up/down you need to scale the pins down/up, in the opposite direction. The counter-scale factor is 1/scale, and you should apply it to each of the <g>s containing the pin image. This lets you remove your current width and height calculation from the <image> nodes (since the parent <g>'s scale will take care of it).
However, for this to work properly, you'll also need to remove the x and y attributes from the <image> and apply position via the counter-scaled parent <g> as well. This is necessary because if there's a local offset (which is the case when x and y are set on the <image>) that local offset gets scaled as the parent <g> is scaled, which makes the pin move to an incorrect location.
So:
var pinContainers = main.select("#pins").selectAll("g")
.attr("transform", function(d) {
var x = ... // don't know how you calculate x (since you didn't show it)
var y = ... // don't know how you calculate y
var unscale = 1/scale;
return "translate(" + x + " " + y + ") scale(" + unscale + ")";
})
pinContainers.select("image")
.attr("width", 43)
.attr("height", 49)
// you can use non-zero x and y to get the image to scale
// relative to some point other than the top-left of the
// image (such as the tip of the pin)
.attr("x", 0)
.attr("y", 0)
I'm trying to use D3.js to draw rectangles around a circle (think of drawing chairs around a table).
I've tried drawing each chair individually by setting it's x and y position and then rotating it, but I haven't had much luck.
I thought this might be a good approach:
group = container.append("g")
table = group.append("circle").
attr("cx", 100).
attr("cy", 100).
attr("r", 60).
attr("fill", "#FFF").
attr("stroke", "#b8b8b8").
attr("stroke-width", "2")
group
.append("rect")
.attr("x", 90)
.attr("y", 10)
.attr("width", 20)
.attr("height", 20)
group.attr("transform", "rotate(30, 100, 100)")
But I can't figure out how to make a transformation, redraw, and then make another transformation. Any ideas?
I ended up building upon my idea of using a rotation. Then, I just had to brush up a little on my geometry.
First, I find a point on the circle. The equation for finding a point on the circle, given you know the center-x and center-y (cx and cy) of the circle, is:
x = cx + (r * sin(a))
y = cx + (r * cos(a))
Where r is radius and a is the angle on the circle in radians.
Then, I drew every rectangle at the point (0, 0) and rotated them around the center of the circle.
Here is what my solution ended up looking like:
radians = (Math.PI * 0) / 180.0
x = 100 + (70 * Math.sin(radians))
y = 100 + (70 * Math.cos(radians))
for i in [0..360] by 30
container
.append("rect")
.attr("x", x - 10)
.attr("y", y)
.attr("width", 20)
.attr("height", 20)
.attr("transform", "rotate(#{i}, 100, 100)")
Note: Subtracting 10 from x accounts for the width of the rectangle.
I am trying to add an Voronoi overlay on my chart, but it seems like when the interval between points are smaller than 20px, some path will never get selected, especially for the points in the start and of the end. Thanks to musically_ut, who notice that this is only happening on Chrome browser.
I went back to play with the original post on Voronoi, and found the same problem (JSFiddle).
var vertices = d3.range(100).map(function(d, i) {
// return [Math.random() * w, Math.random() * h]; //original line
return [i * 10, 10]; // change this line!
});
I thought that this has something to do with the r value of the clipPath, but it was not the case: the problem still persist with a r value of 5.
I went back to the documentation, and turns out that it recommended to add a clipPath when setting up the Voronoi function, which will solve the problem.
Instead of:
d3.geom.voronoi(data)
use
var padding = 0;
var vor = d3.geom.voronoi().clipExtent([[padding, padding], [w - padding, h - padding]])
paths.selectAll("path")
.data(vor(vertices))
JSFiddle
I am posting my workaround on the problem, for anyone who faced a similar situation needing a quick fix. I am still looking for the reason on why this is happening.
My workaround was to create the overlay area manually, in this case the layout logic was fairly easy.
var col = 64;
var cr = 3;
var rwidth = (w / col);
var offset = (rwidth / 2);
// overlay: a rect that is placed around the circle space
svg.selectAll("blank")
.data(d3.range(nbr))
.enter()
.append("rect")
.attr({
"x": function(d,i){ return (i % col) * (rwidth) - offset; },
"y": function(d,i){ return (Math.floor(i / col) + 1) * (rwidth) - offset; },
"width": rwidth,
"height": rwidth,
"fill": "white"
})
.on("mousemove", function(d, i){
// do what you want to do when overlay is triggered
d3.selectAll(".glss" + i)
.transition()
})
// the actual circle
svg.selectAll("blank")
.data(d3.range(nbr))
.enter()
.append("circle")
.attr({
"cx": function(d,i){ return (i % col) * (w / col)},
"cy": function(d,i){ return (Math.floor(i / col) + 1) * (w / col); },
"r": cr,
"fill": function(d,i){ return "#7E07A9"; },
"class": function(d,i){ return "glss glss" + i; }
})
After playing a while with d3.js and looking at a lot of examples, I was able to draw multiple arcs. Each one starting and ending at an specific degree and and given radius.
var dataset = {
"2":[{"degree1":0, "degree2":1.5707963267949,
"label":"Sample Text Test"},
{"degree1":1.5707963267949, "degree2":3.1415926535898,
"label":"Lorem ipsum sample text"},
{"degree1":3.1415926535898, "degree2":4.7123889803847,
"label":"Sample Text Text"},
{"degree1":4.7123889803847, "degree2":6.2831853071796,
"label":"Lorem ipsum"}],
"1":[{"degree1":0, "degree2":3.1415926535898,
"label":"Sample"},
{"degree1":3.1415926535898, "degree2":6.2831853071796,
"label":"Text"}],
"0":[{"degree1":0, "degree2":6.2831853071796,
"label":""}]
},
width = 450,
height = 450,
radius = 75;
// Helper methods
var innerRadius = function(d, i, j) {
return 1 + radius * j;
};
var outerRadius = function(d, i, j) {
return radius * (j + 1);
};
var startAngle = function(d, i, j) {
return d.data.degree1;
};
var endAngle = function(d, i, j) {
return d.data.degree2;
};
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(startAngle)
.endAngle(endAngle);
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + (width >> 1) + ',' + (height >> 1) + ')');
var level = svg.selectAll('g')
.data(function(d) {
return d3.values(dataset);
})
.enter()
.append('g');
var entry = level.selectAll('g')
.data(function(d, i) {
return pie(d);
})
.enter()
.append('g');
entry.append('path')
.attr('fill', '#aaa')
.attr('d', arc)
.attr('id', function(d, i, j) {
return 'arc' + i + '-' + j;
});
var label = entry.append('text')
.style('font-size', '20px')
.attr('dx', function(d, i, j) {
return Math.round((d.data.degree2 - d.data.degree1) * 180 / Math.PI);
})
.attr('dy', function(d, i, j) {
return ((radius * (j + 1)) - (1 + radius * j)) >> 1;
});
label.append('textPath')
.attr('xlink:href', function(d, i, j) {
return '#arc' + i + '-' + j;
})
.style('fill', '#000')
.text(function(d) {
return d.data.label;
});
See http://jsfiddle.net/3FP6P/2/ :
But some problem still exists:
How to center (horizonal und vertical) an text along an textpath of an any length inside an arc described by innerRadius, outerRadius, startAngle and endAngle?
The text occurs sometimes bold, sometimes not. Why?
The character spacing does not appear to be the same as if it is written inside a . Some letters stick more together as other ones. Why?
The letters are not located directly on the path. Some seem to have a little slip up or down. Why?
Vertical alignment
You can use another arc with radius (innerRadius + outerRadius) / 2 and use it as the textPath for the labels.
Note that even if you set the innerRadius == outerRadius, D3 will draw an path which moves clockwise and then anti-clockwise to doubles over itself. This becomes important while trying to figure out the horizontal centre of the path: it is at the 25% and 75% points while 0% and 50% points lie on the two tips of the arc.
Horizontal alignment
Use text-anchor: middle on the text element and set startOffset to 25% (or 75%) on the textPath.
Demo.
This is a more robust way than calculating the dx and dy by hand.
You should try out Lars's suggestions to further improve the quality and centring of the text, e.g. you might want to set text-rendering to optimizeLegibility and play with the baseline a bit.
Issues 2-4 are because of the font rendering. In my browser, the spacing and character size etc is consistent. You can try playing around with the text-rendering attribute to improve things.
To get the text centred, you will need to set the alignment-baseline and/or dominant-baseline attributes.
If this still doesn't give you the results you're looking for, try decreasing the font size. This may help because a slight rotation of a character will be less visible.
I'm generating a musical waveform using D3 by pushing rectangles next to one another. Here's the fiddle: http://jsfiddle.net/s4dML/
var data = [ 0.0534973, /* ...lots and lots of data... */ 0.290771];
data = data.filter(function(datum, index){
return index % 3 == 0;
});
var width = 340,
height = 70,
svg = d3
.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
svg
.selectAll('rect')
.data(data.map(function(datum){
return (datum * height)/2;
}))
// .data(dataset)
.enter()
.append('rect')
.attr('x', function(d, i){
return i * (width / data.length);
})
.attr('y', function(d){
return (height /2) - d ;
})
.attr('width', function(d, i){
return width / data.length;
})
.attr('height', function(d){
return d*2;
})
.attr('fill', 'teal');
Does anyone know why the result ins't single, flat color as expected? There is a kind of shimmering effect throughout. This might be desirable, but regardless I'd like to know how it got there and how to get rid of it if I'm so inclined.
This is an artifact of SVG rendering (or really, any vector graphics rendering). Suppose that you have two rects that meet 40% of the way into a pixel. Then the first rect will paint into that pixel with 40% opacity, and the second with 60% opacity, meaning that the pixel is only (40 + 0.6 * 60 =) 76% colored, even though logically it is 100% covered by colored shapes.
A fix for this is to define the graph as a single <path> object tracing out the top and bottom edges with no "cracks" like this between rects.
I'm not familiar with D3, but in ordinary Javascript:
var path = "M 0," + (height / 2);
for(var i = 0; i < data.length; i++) {
var x = (i + 1) * (width / data.length);
var y = height / 2 - (data[i] * height)/2;
path += " V " + y + " H " + x;
}
for(var i = data.length - 1; i >= 0; i--) {
var x = i * (width / data.length);
var y = height / 2 + (data[i] * height)/2;
path += " V " + y + " H " + x;
}
path += " Z";
Russell's answer is a good one, though you'll end up with a monstrous path. This shouldn't be too much of a problem.
I encountered the same problem the other day when trying to make a bar chart of about 500 data points using very thin bars. The advantage of doing so is that it is much easier to make a mouseover that highlights an individual bar. In cases like this, I find that you have to use integer values for the widths and x positions.
For your example, setting the width and the interval to 1 completely fixes the problem while only making it about 10 percent shorter:
http://jsfiddle.net/s4dML/1/
.attr('x', function(d, i){
return i;// * (width / data.length);
})
.attr('y', function(d){
return (height /2) - d ;
})
.attr('width', function(d, i){
return 1;
})
Of course, this not an extensible solution--just depends on your plans for the widget. I added a mouseover in the above example for demo purposes.