When creating circles using D3, is it possible to create a group such that they can be selected at a later stage? For example, if circles are created using the following approach:
var dataset = [ [ 30, 50, 20],
[ 100, 50, 20],
[ 150, 50, 30]];
//Create SVG element
var svg = d3.select("#chart")
.append("svg")
.attr("width", 200)
.attr("height", 200);
// generate circles
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d){
return d[0];
})
.attr("cy", function(d){
return d[1];
})
.attr("r", function(d){
return d[2];
});
Can I tag the circle created from the first array element with circle1 and the second two circles as circle2?
absolutely - update the class attribute dynamically based on the data index:
.attr("class", function(d,i) {return i == 0 ? "circle1" : "circle2";});
then use the assigned classes for selecting elements:
d3.select(".circle1"); //first circle
d3.selectAll(".circle2"); //second and third circles
Related
I want to change the symbol type from circle to triangle, square, other symbols.
svg.selectAll().
data(data).enter()
.append("circle")
.attr("class", "dot")
.attr("cx", function(d, i) { return timeScale(d.year); })
.attr("cy", function(d, i) { return yScale(d.sale) })
.style("fill", "#FFC300")
.attr("r", function(d) {return est_size(d.est)})
If I change .append("circle") to .append("triangle"), the chart does not show the symbol. How can I show a triangle instead of a circle?
SVG doesn't have an element type for a triangle - the most basic shapes are rect and circle (there are also paths, polygons, ellipses, etc, but no triangle). However, we have a few options open to us, we can use a d3-symbol (available symbols listed here), or we can create our own symbol and use that.
For using d3-symbol we can do the following:
var width = 500;
var height = 300;
var data = d3.range(10)
.map(function(d) { return { x: Math.random()*width, y: Math.random()*height }; })
var svg = d3.select("svg")
.attr("width",width)
.attr("height",height);
svg.selectAll(".symbol")
.data(data)
.enter()
.append("path")
.attr("d", d3.symbol().type(d3.symbolTriangle).size(50))
.attr("transform",function(d) { return "translate("+[d.x,d.y]+")" })
.attr("class","symbol");
// For demonstrating that the triangles are centered:
svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("r", 3)
.attr("fill","orange")
.attr("transform",function(d) { return "translate("+[d.x,d.y]+")" });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
symbol.size corresponds to shape area, not an edge length
Alternatively, we can create a function that returns a basic triangle polygon ourselves, and use it with selection.append():
var width = 500;
var height = 300;
var data = d3.range(10)
.map(function(d) { return { x: Math.random()*width, y: Math.random()*height }; })
var svg = d3.select("svg")
.attr("width",width)
.attr("height",height);
var symbol = function() {
// Hand drawn triangle:
return d3.create('svg:path').attr("d","M0,8L-5,-3L5,-3Z").node()
}
svg.selectAll(".symbol")
.data(data)
.enter()
.append(symbol) // append can accept a function.
.attr("transform",function(d) { return "translate("+[d.x,d.y]+")" })
.attr("class","symbol");
// For demonstrating that the triangles are centered:
svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("r", 3)
.attr("fill","orange")
.attr("transform",function(d) { return "translate("+[d.x,d.y]+")" });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
We could also take a few other approaches, such as using svg symbol elements, but the above two methods should be sufficient.
I want to repeat a group of shapes specifically
text rect text circles
Where in circles is again a repeat of circle
My data is
Jsondata =[
{ "name":"A", "WidthOfRect":50, "justAnotherText":"250", "numberOfCircles" :3 },
{ "name":"B", "WidthOfRect":150, "justAnotherText":"350","numberOfCircles" :2 },
{ "name":"C", "WidthOfRect":250, "justAnotherText":"450","numberOfCircles" :1 }]
Basically Out of this data i am trying to construct a customized bar chart.
The width of the rect is based upon the data widthofrect from the json, as well as number of circles is based upon numberofcircles property.
I looked out for a number of options to repeat group of shapes but couldn't find one.
First of all, you're right in your comment: do not use loops to append elements in a D3 code. Also, your supposition about the length of the data is correct.
Back to the question:
The text and rect part is pretty basic, D3 101, so let's skip that. The circles is the interesting part here.
My proposed solution involves using d3.range to create an array whose number of elements (or length) is specified by numberOfCircles. That involves two selections.
First, we create the groups (here, scale is, obviously, a scale):
var circlesGroups = svg.selectAll(null)
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(20," + scale(d.name) + ")"
});
And then we create the circles. Pay attention to the d3.range:
var circles = circlesGroups.selectAll(null)
.data(function(d) {
return d3.range(d.numberOfCircles)
})
.enter()
.append("circle")
//etc...
Here is a demo, I'm changing the numberOfCircles in your data to paint more circles:
var width = 500,
height = 200;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var data = [{
"name": "A",
"WidthOfRect": 50,
"justAnotherText": "250",
"numberOfCircles": 13
},
{
"name": "B",
"WidthOfRect": 150,
"justAnotherText": "350",
"numberOfCircles": 22
},
{
"name": "C",
"WidthOfRect": 250,
"justAnotherText": "450",
"numberOfCircles": 17
}
];
var scale = d3.scalePoint()
.domain(data.map(function(d) {
return d.name
}))
.range([20, height - 20])
.padding(0.5);
var colorScale = d3.scaleOrdinal(d3.schemeCategory10);
var circlesGroups = svg.selectAll(null)
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(20," + scale(d.name) + ")"
})
.style("fill", function(d) {
return colorScale(d.name)
})
var circles = circlesGroups.selectAll(null)
.data(function(d) {
return d3.range(d.numberOfCircles)
})
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d) {
return 10 + 12 * d
});
var axis = d3.axisLeft(scale)(svg.append("g").attr("transform", "translate(20,0)"));
<script src="https://d3js.org/d3.v5.min.js"></script>
PS: I'm using D3 v5.
I am a very beginner programmer (emphasis on very). I am trying to figure out how to iterate through an array of colors in order to make three rectangles of different colors. Here is my code so far:
var dataArray = [5, 11, 18];
var colors = ["red", "green", "black"];
var svg = d3.select("body").append("svg")
.attr("width","2000").attr("height","400");
svg.selectAll("rect")
.data(dataArray)
.enter()
.append("rect")
.attr("fill", function(d, i) { return colors*i}) //the line of code in question
.attr("x", function(d,i) { return 70*i + 50; })
.attr("y", function(d,i) { return d*15; })
.attr("height", function(d,i) { return 500; })
.attr("width", "50");
//code end
As you can see, I've been trying to use function(d, i) to iterate through array colors, unsuccessfully. Full disclosure: the above code was created for a class, but this particular question is not part of the assignment. I'm trying to go just a tiny bit beyond the assignment.
The other answer is right, colors[i] will give you your colors, so it deserves the checkmark. The fix in that answer will give you:
var dataArray = [5, 11, 18];
var colors = ["red", "green", "black"];
var svg = d3.select("body").append("svg")
.attr("width","2000").attr("height","400");
svg.selectAll("rect")
.data(dataArray)
.enter()
.append("rect")
.attr("fill", function(d, i) { return colors[i]})
.attr("x", function(d,i) { return 70*i + 50; })
.attr("y", function(d,i) { return d*15; })
.attr("height", function(d,i) { return 500; })
.attr("width", "50");
//code end
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
But, looking at the results, the first bar is the tallest, but has the smallest data value. All bars are 500 pixels high, each has a part that is below the edge of the SVG.
I thought I'd just point out a couple potential improvements on your code:
The use of function(d,i) is only necessary when grabbing either the current element in your data array (d) or the current increment (i), so for:
.attr("height", function(d,i) { return 500; }) you can use: .attr('height',500);
But, I doubt you want all items to be 500 pixels tall (especially as your svg is only 400 pixels tall). This will be especially apparent if you have margins.
So, for height, we can use the formula you are currently using for your y coordinate:
.attr("height", function(d,i) { return d*15; })
Now, we have to have the bars end at the same point by manipulating the position of the top of each rectangle:
(go up d*15 pixels from the bottom of the svg (which is at 400), in svg coordinate space 0 is at the top):
.attr("y", function(d,i) { return 400 - d*15; })
Which gives you:
var dataArray = [5, 11, 18];
var colors = ["red", "green", "black"];
var svg = d3.select("body").append("svg")
.attr("width","2000").attr("height","400");
svg.selectAll("rect")
.data(dataArray)
.enter()
.append("rect")
.attr("fill", function(d, i) { return colors[i]})
.attr("x", function(d,i) { return 70*i + 50; })
.attr("y", function(d,i) { return 400-d*15; })
.attr("height", function(d,i) { return d*15; })
.attr("width", "50");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
Lastly,
Quotations enclose strings, but if you are using numbers then you can drop them:
This: .attr("width", "50"); can be .attr("width",50);
Have you tried using colors[i] instead of colors*i?
That will allow you to access the value in place i.
(colors[1] will be 'green', colors[0] will be 'red' etc)
I'm trying to recreate the map shown here using D3 but I'm having trouble binning the values from my data and mapping them to colors. Currently I'm using scale.quantile that I believe sets the number of bins according to the range which should be 8 since I have 8 colors. So it would bin up the fertility values into 8 different bins and map each bin to each color but the colors are incorrect. In my data Sweden has a fertility rate of 1.89 and Russia 1.7 but they are the same color on my map which looks like this with my current code. What am I doing wrong?
var width5 = 700;
var height5 = 500;
buckets = 8,
colors = ['#ff1a1a','#ff471a','#ffd633','#ffff1a','#ccff33',' #66ff66','#00ff00',' #009900'];
var projection = d3.geo.mercator()
.center([ 13, 52 ])
.translate([ width5/2.5, height5/1.7 ])
.scale([ width5/1.5 ]);
var path = d3.geo.path()
.projection(projection);
var mapchart = d3.select("#mapchartarea")
.append("svg")
.attr("width", width5)
.attr("height", height5);
d3.json("data/europe_geo_fertility.json", function(error,collection) {
if(error) throw error;
console.log(d3.min(collection.features, function(d) { return d.properties.fertility;}))
// Sets color for each country according to fertility rate
var colorScale = d3.scale.quantile()
.domain([0, buckets -1, d3.max(collection.features, function(d) { return d.properties.fertility;})])
.range(colors);
// Draws Map
mapchart.selectAll("path")
.data(collection.features)
.enter()
.append("path")
.attr("d", path)
.attr("stroke", "rgba(8, 81, 156, 0.2)")
.attr("fill", "rgba(8, 81, 156, 0.6)")
.style("fill", function(d) { return colorScale(d.properties.fertility); });
// Adds name of each country
mapchart.selectAll("text")
.data(collection.features)
.enter()
.append("svg:text")
.text(function(d){
return d.properties.name;
})
.attr("x", function(d){
return path.centroid(d)[0];
})
.attr("y", function(d){
return path.centroid(d)[1];
})
.attr("text-anchor","middle")
.attr('font-size','6pt');
});
I am pretty new to d3. For the moment I am able to draw circles based on an array of data - wow - I know :-) But now I would like to just draw two circles at one time while I animate the whole array. Let's say I have 1000 elements in my array and I want to draw 1 and 2 at the same time, then draw 2 and 3, 3 and 4 and so on. This should get a very pretty animation :-) I have played with functions i index and with exit().remove() but this does not work.
This is what I have:
var w = 500;
var h = 300;
var padding = 20;
var dataset = [
[5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
[410, 12], [475, 44], [25, 67], [85, 21], [220, 88],
[600, 150]
];
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Create scale functions
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) { return d[0]; })])
.range([padding, w - padding * 2]);
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) { return d[1]; })])
.range([h - padding, padding]);
//Create circles
svg.selectAll("circle")
.data(dataset.slice(0,2))
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r",10);
for (var i=0; i<4;i++) {
svg.selectAll("circle").data(dataset.slice(i,i+2)).transition().duration(2000).delay(2000)
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", 10);
//svg.selectAll("circle").data(dataset.slice(i,i+1)).exit().remove();
console.log(dataset.slice(i,i+2));
}
But I will get only one single animation instead of 4 .. hmm .. what is going wrong?
The delay function accepts callbacks, so there is no need to wrap your selection in a for loop.
.delay( function(d, i) { (2000*i); } )
Looking at the code, you've got a fixed duration (2s) and a fixed delay (2s). The FOR loop will run instantly, thus queueing all four animations up at once, and thus they are probably all playing at the same time - but (probably) only the last will be visible because you've rebound the data.
try something like:
svg.selectAll("circle")
.delay( (2000*i) )
.data(dataset.slice(i,i+2))
.transition()
.duration(2000)
.attr("cx", function(d) {return xScale(d[0]);})
.attr("cy", function(d) {return yScale(d[1]);})
.attr("r", 10);)
Multiplying the delay by the animation counter should offset each animation, and by putting the delay first, it should the data gets rebound just before starting the animation (thereby stopping the final animation step from rebinding it's data before the first animation has run)