Can d3 build shapes within a shape? - d3.js

I have a newby question. Can D3 draw this:
http://www.nytimes.com/interactive/2008/05/03/business/20080403_SPENDING_GRAPHIC.html?_r=0
using the voronoi function within d3? What i am thinking is a svg that behaves like a and binds the voronoi found here http://bl.ocks.org/mbostock/4060366 to a circle. NY Times has accomplished the above visualization using flash.
Any ideas?
I have tried creating a large circle and embeding the smaller circles, but the voronoi does not show up and the points are not confined to the outer circle.
Code generated:
<svg class="PiYG" width="560" height="570">
<circle cx="270" cy="300" r="260" style="stroke: rgb(0, 0, 0);">
<g>
My js code looks something like this:
var width = 560, height = 570;
var svg = d3.select("#VD1").append("svg")
.attr("width", width)
.attr("height", height)
.attr("class", "PiYG");
var path = svg.append("circle")
.attr("cx", 270)
.attr("cy", 300)
.attr("r", 260)
.style("stroke", "#000")
.append("g")
.selectAll("path");
var vertices = d3.range(count).map(function(d) {
return [Math.random() * width, Math.random() * height];
});
var voronoi = d3.geom.voronoi()
.clipExtent([[0, 0], [width, height]]);
svg.selectAll("circle")
.data(vertices.slice(2))
.enter().append("circle")
.attr("transform", function(d) { return "translate(" + d + ")"; })
.attr("r", 2);
Thanks so much!

Not really, but not due to any shortcomings of d3, but rather because that's not what a Voronoi function does. A Voronoi function builds lines based on which regions of a graph are closest to a given point. It is not a way of proportionately dividing a circle into smaller segments, with size corresponding to data.
That being said, it is definitely possible to create a circular Voronoi diagram. To do so, you'll have to make a few changes to the example Voronoi diagram code.
First you'll have to make sure that all of the points fit in a circle. In your example, point location is given by d, here:
.attr("transform", function(d) { return "translate(" + d + ")"; })
Either d (your dataset) needs to fit in the circle, or you need to make some transformation of it. For data normalized to (-1,1) in both dimensions, the function
.attr("transform", function(d) { return "translate([" +
d[0]*Math.sqrt(1 - Math.pow(d[1],2)/2)
+ "," +
d[1]*Math.sqrt(1 - Math.pow(d[0],2)/2)
+ "])"; })
will do so. Here, we've created a new array that will be bounded by a circle from the array originally in d.
Next, you would need to clip your Voronoi diagram to be contained within a circle. The fun part is, there isn't a built in 'circle' geometric object to clip with, so you'll need to get creative! Either build a custom way to do this, or let the Voronoi extend beyond your circle and build an SVG to cover it up. Either should work.

Related

Add text in rect svg and append it to arc in donut chart

I wanted to add labels to each arc in donut chart. I've added by taking the centroid of each arc and adding, but somehow it is not adding in correct position. I can't figure it out so I need some help regarding it. I've added my code in codepen. The link is here.
My donut should look like this.
Sample code is:
svg.selectAll(".dataText")
.data(data_ready)
.enter()
.each(function (d) {
var centroid = arc.centroid(d);
d3.select(this)
.append('rect')
.attr("class", "dataBG_" + d.data.value.label)
.attr('x', (centroid[0]) - 28)
.attr('y', (centroid[1]) - 5)
.attr('rx', '10px')
.attr('ry', '10px')
.attr("width", 50)
.attr("height", 20)
.style('fill', d.data.value.color)
.style("opacity", 1.0);
d3.select(this)
.append('text')
.attr("class", "dataText_" + d.data.value.label)
.style('fill', 'white')
.style("font-size", "11px")
.attr("dx", (centroid[0]) - 7)
.attr("dy", centroid[1] + 7)
.text(Math.round((d.data.value.value)) + "%");
});
Thanks in advance.
The difference between the "bad" state on codepen and the desired state is that in the one you don't like, you take the centroid and then you center your text on it. The centroid of a thick arc is the midpoint of the arc that runs from the midpoint of one line-segment cap to the other. This is roughly "center of mass" of the shape if it had some finite thickness and were a physical object. I don't think it's what you want. What you want is the midpoint of the outer arc. There's no function to generate it, but it's easy enough to calculate. Also, I think you want to justify your text differently for arcs whose text-anchor point is on the left hand of the chart from those on the right half. I'm going copy your code and modify it, with comments explaining.
// for some reason I couldn't get Math.Pi to work in d3.js, so
// I'm just going to calculate it once here in the one-shot setup
var piValue = Math.acos(-1);
// also, I'm noting the inner radius here and calculating the
// the outer radius (this is similar to what you do in codepen.)
var innerRadius = 40
var thickness = 30
var outerRadius = innerRadius + thickness
svg.selectAll(".dataText")
.data(data_ready)
.enter()
.each(function (d) {
// I'm renaming "centroid" to "anchor - just a
// point that relates to where you want to put
// the label, regardless of what it means geometrically.
// no more call to arc.centroid
// var centroid = arc.centroid(d);
// calculate the angle halfway between startAngle and
// endAngle. We can just average them because the convention
// seems to be that angles always increase, even if you
// if you pass the 2*pi/0 angle, and that endAngle
// is always greater than startAngle. I subtract piValue
// before dividing by 2 because in "real" trigonometry, the
// convention is that a ray that points in the 0 valued
// angles are measured against the positive x-axis, which
// is angle 0. In D3.pie conventions, the 0-angle points upward
// along the y-axis. Subtracting pi/2 to all angles before
// doing any trigonometry fixes that, because x and y
// are handled normally.
var bisectAngle = (d.startAngle + d.endAngle - piValue) / 2.0
var anchor = [ outerRadius * Math.cos(bisectAngle), outerRadius * Math.sin(bisectAngle) ];
d3.select(this)
.append('rect')
.attr("class", "dataBG_" + d.data.value.label)
// now if you stopped and didn't change anything more, you'd
// have something kind of close to what you want, but to get
// it closer, you want the labels to "swing out" from the
// from the circle - to the left on the left half of the
// the chart and to the right on the right half. So, I'm
// replacing your code with fixed offsets to code that is
// sensitive to which side we're on. You probably also want
// to replace the constants with something related to the
// the dynamic size of the label background, but I leave
// that as an "exercise for the reader".
// .attr('x', anchor[0] - 28)
// .attr('y', anchor[1] - 5)
.attr('x', anchor[0] < 0 ? anchor[0] - 48 : anchor[0] - 2)
.attr('y', anchor[1] - 10
.attr('rx', '10px')
.attr('ry', '10px')
.attr("width", 50)
.attr("height", 20)
.style('fill', d.data.value.color)
.style("opacity", 1.0);
d3.select(this)
.append('text')
.attr("class", "dataText_" + d.data.value.label)
.style('fill', 'white')
.style("font-size", "11px")
// changing the text centering code to match the box
// box-centering code above. Again, rather than constants,
// you're probably going to want something a that
// that adjusts to the size of the background box
// .attr("dx", anchor[0] - 7)
// .attr("dy", anchor[1] + 7)
.attr("dx", anchor[0] < 0 ? anchor[0] - 28 : anchor[0] + 14)
.attr("dy", anchor[1] + 4)
.text(Math.round((d.data.value.value)) + "%");
});
I tested. this code on your codepen example. I apologize if I affected your example for everyone - I'm not familiar with codepen and I don't know the collaboration rules. This is all just meant by way of suggestion, it can be made a lot more efficient with a few tweaks, but I wanted to keep it parallel to make it clear what I was changing and why. Hope this gives you some good ideas.

D3js Zoom With Manually Drawn Circle

I am working on a d3 scatter plot where an area of the chart will be circled (a Youden Plot). Based on available samples, I have been able to add zoom to both my data points and my axis. However, I am unable to get the circle to zoom correctly.
I suspect that I need to set up some kind of scale (scaleSqrt, possibly), but I am struggling to find documentation on this that is written at a beginner level.
My current circle code is very straightforward
var circle = drawCircle();
function drawCircle() {
return svg
.append('g')
.attr('class', 'scatter-group')
.append('circle')
.attr("r", 75 )
.attr('cx', 200 + margin.left) //suspect this needs to be related to a scale
.attr('cy', 200 + margin.top) //suspect this needs to be related to
.attr('r', 75)//suspect this needs to be related to a scale
.attr('stroke', 'red')
.attr('stroke-width', 3)
.style('fill', 'none')
}
As is the zoomed function
function zoomed() {
var new_xScale = d3.event.transform.rescaleX(xScale);
var new_yScale = d3.event.transform.rescaleY(yScale);
// update axes
gX.call(xAxis.scale(new_xScale));
gY.call(yAxis.scale(new_yScale));
//redraw data ppints
points.data(data)
.attr('cx', function(d) {return new_xScale(d.x)})
.attr('cy', function(d) {return new_yScale(d.y)});
//redraw circle
}
My work in progress is available in this fiddle . Can someone possible point me in the right direction?
I believe this will get you most of the way there. You need to update your circle attributes in the zoomed function along with the other elements:
function zoomed() {
var new_xScale = d3.event.transform.rescaleX(xScale);
var new_yScale = d3.event.transform.rescaleY(yScale);
// update axes
gX.call(xAxis.scale(new_xScale));
gY.call(yAxis.scale(new_yScale));
//redraw data ppints
points.data(data)
.attr('cx', function(d) {return new_xScale(d.x)})
.attr('cy', function(d) {return new_yScale(d.y)});
// The new part:
// the transform
let trans = d3.event.transform
// the approximate domain value of the circle 'cx' for converting later
let cx_domain = xScale.invert(200 + margin.left)
// the approximate domain value of the circle 'cy' for converting later
let cy_domain = yScale.invert(200 + margin.top)
// the circle
let circ = d3.select('.scatter-group circle')
// the radius
let rad = 75
// reset the circle 'cx' and 'cy' according to the transform
circ
.attr('cx',function(d) { return new_xScale(cx_domain)})
.attr('cy',function(d) { return new_yScale(cy_domain)})
// reset the radius by the scaling factor
.attr('r', function(d) { return rad*trans.k })
}
See this fiddle
You'll notice the circle does not scale or move at quite the same rate as the scatter dots. This is possibly because of the use of the invert function, because the conversion from range to domain and back to range is imperfect. This issue is documented
For a valid value y in the range, continuous(continuous.invert(y)) approximately equals y; similarly, for a valid value x in the domain, continuous.invert(continuous(x)) approximately equals x. The scale and its inverse may not be exact due to the limitations of floating point precision.
Your original idea to assign dynamic values to cx, cy and r will likely compensate for this, because you can then avoid the inversion.

Synchronize an image with the rest of the svg with D3js

I am trying to plot some points on a map. My map is a svg generated separately (in the PATH_PARAM file). It is supposed to act as a background, with details that must be at specific coordinates in the final plot. Even if it's not exactly that you can imagine the map is a picture of a face I traced myself (so I know its size and where are each elements in the map's referential) and I want to add points precisely in the middle of the eyes after importing it on my svg with D3.js.
For now I simply append it to the svg, like so: (this javascript is called during the loading of the page)
var SIZE_PARAM = 200;
var DATA_PARAM = [{position: [0,1]}, {position: [4,5]}] //actually it's a far more bigger set of points.
var graph = d3.select("#graph");
svg = graph.append("svg")
.attr("width",window.innerWidth)
.attr("height",window.innerHeight)
.call(d3.behavior.zoom().on("zoom",
function () {svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")")}
))
.append("g");
background = svg.selectAll("image")
.data([0])
.enter()
.append("svg:image");
background.attr("xlink:href", PATH_PARAM)
.attr("width",SIZE_PARAM)
.attr("height",SIZE_PARAM);
scale = d3.scale.linear()
.domain([dom_x_min, dom_x_max])
.rangeRound([ 0, SIZE_PARAM ])
.clamp(true);
circles = svg.selectAll("circle")
.data(DATA_PARAM)
.enter()
.append("circle");
circles.attr("class", "point")
.attr("cx", function(d,i) {return scale(d.position[0]);})
.attr("cy", function(d,i) {return scale(d.position[1]);})
.attr("r", function(d,i) {return 1;})
The problem I got is that the map and the points are not synchronized with the map. The relative position between points is good, and I managed to have the same scale for the map and the points by setting a unique SIZE_PARAM parameter, but there is a translation between the points and the map. This translation is not visible for a small number of points (like the two of the example) but becomes more and more noticeable as this number increases.
Following some advice I added this to the CSS:
#graph * {
position: absolute;
top: 0px;
left: 0px;
}
But it doesn't solve my problem.
What can I do to synchronize the map with the points ?

Trying to append an ellipse to svg to cover points

I have produced a scatter chart using D3. It works okay. Now I want to cover the points plotted with an ellipse.
I've tried to get the xOval value by looking for the max value in the dataset, same for yOval.
var xOval = d3.scale.linear()
.domain([0,d3.max(dataset, function(d) { return d[0]; })])
var yOval = d3.scale.linear()
.domain([0,d3.max(dataset, function(d) { return d[1]; })])
Further down in the body after the scatter is plotted I have :
svg.append("ellipse")
.attr("cx", function(d) {
return xOval(d[0]);
})
.attr("cy", function(d) {
return yOval(d[1]);
})
.attr("rx", 10)
.attr("ry", 10);
This fails to work.
Any ideas where I'm going wrong?
This is quite difficult to do with an ellipse, as you have to make sure that everything is within the path defined by it. Here's how to do it with a circle, which should get you started.
The easiest way to do this is to use D3's polygon methods for this. In particular, you can get the centroid of the polygon defined by all points as the center of the circle. Then all you have to do is find the distance to the point that is farthest away from that and you have your radius.
The code looks something like this, assuming suitable data structures.
var center = d3.geom.polygon(dataset).centroid(),
r = d3.max(dataset, function(d) {
return Math.sqrt(Math.pow(xScale(d[0]) - xScale(center[0]), 2) + Math.pow(yScale(d[1]) - yScale(center[1]), 2));
}) + rScale;
rScale here is the radius of the circles that represent a point. The radius of the covering circle needs to be extended by that to make sure that not only the center, but the entire data point circle is covered.
Complete example here. Note that this is not optimal in the sense that there would be a smaller circle that covers all data points.

How to limit the text of polygons in Voronoi diagram with D3.js?

I've see the Example of D3.js-Voronoi Tessellation.But I want to put some text in each of polygons instead of a circle,Here is my js code:
var width = 600, height = 400;
var vertices = d3.range(20).map(function(d){
return [Math.random() * width, Math.random() * height]
});
var voronoi = d3.geom.voronoi();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
path = svg.append("g").selectAll("path");
svg.selectAll("info")
.data(vertices.slice(1))
.enter().append("text")
.attr("transform", function(d) {
return "translate(" + d + ")";
})
.text("someText")
.attr("shape-rendering","crispEdges")
.style("text-anchor","middle");
redraw();
function redraw(){
path = path
.data(voronoi(vertices), polygon);
path.exit().remove();
path.enter().append("path")
.attr("class", function(d, i) {return "q" + (i % 9) + "-9";})
.attr("d", polygon);
path.order();
}
function polygon(d){
return "M" + d.join("L") + "Z";
}
I have a JSFiddle for that basic example here:
my voronoi code
now, I want each of the polygons' text in the center of the polygon, and don't cross with the polygon's border. If the polygon have not enough space to contain the all text, just contain the first part of it!
Let me know if there is anything I can do to solve this issue, thank you!
PS:I'm so sorry to my English, yes, it's so poor! :)
Have a look at this example http://bl.ocks.org/mbostock/6909318 , you probably want to place the text at the polygon centroid and not the seed (point) used to determine the voronoi tessellation.
That should fix the majority of your layout issues.
Automatically scaling the text to fit is a little bit harder, if you are willing to scale and rotate the text you can use a technique similar to the following to determine the length of the line at that point:
https://mathoverflow.net/questions/116418/find-longest-segment-through-centroid-of-2d-convex-polygon
Then you need to determine the angle of the line. I have a plugin that should help with that:
http://bl.ocks.org/stephen101/7640188/3ffe0c5dbb040f785b91687640a893bae07e36c3
Lastly you need to scale and rotate the text to fit. To determine the width of the text use getBBox() on the text element:
var text = svg.append("svg:text")
.attr("x", 480)
.attr("y", 250)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.style("font", "300 128px Helvetica Neue")
.text("Hello, getBBox!");
var bbox = text.node().getBBox();
Then you use the angle you calculated earlier to scale and rotate your text:
text.attr("transform", "rotate(40) scale(7)")
I would love to give a complete example but this is quite a bit of work to get it right.
There are other options to achieve the same effect but none of them are simple (ie you could anneal the layout similar to the way d3 does the Sankey layout)

Resources