Creating variable number of elements with D3.js - javascript

I am pretty new to d3.js and maybe my question is very basic, but I haven't been able to find an answer...
I am trying to achieve the following, based on an array of data like this:
var data = [
{
"items": 10,
"selected": 8
},
{
"items": 12,
"selected": 4
}];
I would like to create a row of circles for every element of the array. The number of circles should be the equal to the items property and in every row, the circle in selected position should be special (like a different color). For the example above, it should display something similar to:
OOOOOOO*OO
OOO*OOOOOOOO
For the first step, any tips on how to create a variable number of SVG elements based on data values would be a great help.

Here's an example I made on codepen.
Check out the code below and/or fork the codepen and have a play.
Essentially, what is happening here is that I add a g element for each item in your data array. Normally we might be-able to create a circle for each data element, but since that is contained within a property and variable, I've used an each loop (d3 each). This creates a loop of the number of items, and creates a circle for each. If the element is selected, the fill color changes.
things to note:
The g element has a transform of 30 * i on the y axis. This means that each group will stack down the page.
In the for loop we get access to the g element using this, but we must use the d3 select function to reference it as a d3 object so we can append.
The code:
//add an svg canvas to the page
var svg = d3.select("body")
.append("svg")
.attr("transform", "translate(" + 20 + "," + 20 + ")"); //this will give it a bottom and left margin
//set up some basic data
var data = [
{
"items": 10,
"selected": 8
},
{
"items": 12,
"selected": 4
}];
var groups = svg.selectAll("g")
.data(data)
.enter()
.append("g").attr("transform", function(d, i) {
return "translate(0," + i * 30 + ")";
});
groups.each(function(d,i) {
for (var x = 1; x <= d.items; x++) {
d3.select(this).append('circle').attr({cx: function(d,i) { return (x + 1) * 22; } ,
cy: 10 ,
r: 10 ,
fill: function(d,i) { return x == d.selected ? "yellow" : "blue" }
});
}
});

Related

D3: Animate circle along border of country on spinning globe

My problem is simple to explain but I am having real trouble implementing a solution. I am trying to animate a circle along a path on a D3 map. The twist here is that I would like to use one of Mike Bostock's spinny globes (i.e. 3D map).
In time, I would like to add other paths to the globe and to use these for my animations. For now, I would simply like to animate the circles along the border of Russia (i.e. along the path of the Russia polygon coordinates)
I have built a jsfiddle to get traction on this and you can see all my code. Unfortunately I cannot get it to work, and am hoping you can help me out. My jsfiddle: http://jsfiddle.net/Guill84/xqmevpjg/7/
I think my key difficulty is (a) actually referencing the Russia path, and I think I am not getting it right at the moment, and (b) making sure that the interpolation is calculated properly (i.e. that the animation is dynamically linked to the globe, and not just 'layered on top'). The code that is supposed to do that is as follows:
setTimeout(function(){
var path = d3.select("path#Russia"),
startPoint = pathStartPoint(path);
marker.attr("r", 7)
.attr("transform", "translate(" + startPoint + ")");
transition();
//Get path start point for placing marker
function pathStartPoint(path) {
var d = path.attr("d"),
dsplitted = d.split(" ");
return dsplitted[1].split(",");
}
function transition() {
marker.transition()
.duration(7500)
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);// infinite loop
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";//Move marker
}
}
}
I'd be hugely grateful for any help.
For the first part of your question, one way to select the path is to add an id to id :
d3.json("http://mbostock.github.io/d3/talk/20111018/world-countries.json", function(collection) {
feature = svg.selectAll("path")
.data(collection.features)
.enter().append("svg:path")
.attr("d", clip)
.attr("id", function(d) { return d.properties.name; }) ;
and then select the path like that :
var path = d3.select("#Russia").node()
Then you can select the first point with :
path.getPointAtLength(0)
See this updated fiddle : http://jsfiddle.net/xqmevpjg/11/

d3 NaN when using javascript Filter (data.filter) on stacked Bar Chart

I have a stacked bar chart in d3. Each stack is made up of 4 svg rectangles. I would like to create a text box above each stack in the chart with a stack total.
The last rectangle in the stack data[i][3] contains elements x, y and y0.
i represents stacks 0 through 3.
Here's a console output for :
.data(data.filter(function(d,i){
console.log("This is i: " + i);
console.dir(d[3]);
return d[3];
}))
This is i: 0
x: 0
y: 0.46
y0: 2.4
The idea is that I will be able to create a dataset for just the last rectangle of each stack and then find the y position for the text box by adding y and y0 for that rectangle.
I am able to do this using the d3 .filter(":last-child") option, but it creates empty rect elements which I do not want. When I try a javaScript filter I get a NaN result for d.x and d.y values. Any advice?
stack.selectAll('.stackTotalBox')
.data(data.filter(function(d,i){
console.log("This is i: " + i);
console.dir(d[3]);
return d[3];
}))
.enter()
.append("rect")
// .filter(":last-child") -- this works but generates empty rect
.attr("class","stackTotalBox")
.attr("x", function (d) {
console.dir(d);
return xScale(d.x) + 170/2 - 39.12; // + width of stack/2 - width of text box/2
})
.attr("y", function (d) {
return yScale(d.y0 + d.y) - 40; // height of stack + room for text box
})
.attr("width", 78.24) // width of text box
.attr("height", 19.347) // height of text box
.attr("rx", 8) // rounded corners
.attr("ry", 8) // rounded corners
.attr("fill", "#BBBDBF");
Update with data structure of data
Data contains 4 arrays (one array per stack), each array has 4 objects(rectangles), Each object contains coordinates for a rectangle.
This is how it looks in console with the first stack/array expanded:
Array[4]
--> 0: Array[4] (This represents a Stacked bar)
----> 0: Object
----> 1: Object
----> 2: Object
----> 3: Object (This Represents the last rectangle in the stacked bar)
--------> x: 0 (First Array has x = 0, 2nd Array x = 1)
--------> y: 0.4 (height of rectangle)
--------> y0: 2.13 (height of preceding rectangles, starting Y coordinates)
--> 1: Array[4]
--> 2: Array[4]
--> 3: Array[4]
I see. You should use Array.prototype.map instead of Array.prototype.filter. if you use filter then your output will be included whenever closure return true.
So in your case, since d[3] is an object and it's always true, you basically didn't filter anything and thus d is still a 2-d array, and d.x is undefined.
Try this to filter your data:
stack.selectAll('.stackTotalBox')
.data(data.map(function(item){
return item[3];
}))

Color scale not working appropriately in D3/JavaScript [duplicate]

This question already has answers here:
d3.scale.category10() not behaving as expected
(3 answers)
Closed 8 years ago.
Im using a color scale :
var color = d3.scale.category10();
Im using this to color the edges of a force layout graph according to their value
var links = inner.selectAll("line.link")
.style("stroke", function(d) { return color(d.label); });
Now I need a 'legend' to show the user what each color means:
edgesArray = [];
edgesArrayIndex = [];
for (i=0;i<data.edges.length;i++) {
if(!edgesArray[data.edges[i].name])
{
edgesArray[data.edges[i].name]=1;
edgesArrayIndex.push(data.edges[i].name);
}
}
var colourWidth = 160;
var colourHeight = 25;
for(i=0; i<edgesArrayIndex.length; i++){
if (edgeColour == true){
svg.append('rect')
.attr("width", colourWidth)
.attr("height", colourHeight)
.attr("x", Marg*2)
.attr("y", Marg*2 + [i]*colourHeight)
.style("fill", color(i))
;
svg.append("text")
.attr("x", Marg*3)
.attr("y", Marg*2 + [i]*colourHeight + colourHeight/2)
.attr("dy", ".35em")
.text(color(i) + " : " + data.edges[i].name);
}
//console.log(edgesArrayIndex);
}
The colors are all wrong. When I attach the colors to the graph the first 6 colors get attached to the edges as there are 6 different types of edges.
But when I apply the color scale to the 6 'rects' I appended to the SVG its as if the first 6 colors of the color scale array got used up when applying then to the edges and when i do the for loop starting at color(0) it actually gives me the color at color(5) (as 0 is first).
For example, say ive got red,blue,green,white,black,pink,orange,yellow,indigo,aqua.
My edges get - red,blue,green,white,black,pink
and now when i want to apply the scale to the rects I made, I would expect the rects to have the same values - red,blue,green,white,black,pink.
But they actually have : orange,yellow,indigo,aqua, red, blue.
They start at color[5] and its as if they wrap round to the beginning and go back to red, blue and so on.
You are using two different parts of the data for the colour scale -- first the label, then the name. By definition, this will give you different colours (different inputs map to different outputs as far as possible given the output range).
To get the same colours, pass in the same attributes, e.g. the labels (if I understood your data structure correctly):
svg.append('rect')
.style("fill", color(data.edges[i].label))
this question helped me -
d3.scale.category10() not behaving as expected
basically you have to put a domain on the scale. So instead of
var color = d3.scale.category10();
Change it to:
var color = d3.scale.category10().domain([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);//-colour scale

d3: position text element dependent on length of element before

I'm stuck in d3 (or JavaScript in general).
I want to make a legend with d3. The position of the 9 items should be dependent on each other. More specifically:
This is my simplified array:
var dataset = ["Africa","Asia", "Caribbean", "Central America", "Europe", "Middle East", "North America", "Oceania", "South America"];
On the x-axis, I want to draw the next text 40px futher (to the right) then the last text lable ended. My intention is to have the same space between the circles every time. So the next text is always dependent on the length of the last country name.
I tried this:
.attr("x", function(d, i) {return i * 40 + d[i-1].length + 7;})
but the console says d[i-1] is undefined.
What am I missing? How would you solve this?
Many thanks in advance! Your help is very much appreciated!
Ewa
UPDATE:
Actually the legend I want to draw not only consists of the text, but also little circles.
Here is the array (with hard coded x_pos as d[2]: var dataset = [
["Africa", "#4B7985", 5], ["Asia", "#58AB86", 55], ["Caribbean", "#63A4B5", 100], ["Central America", "#818181", 165], ["Europe", "#E9726C", 255], ["Middle East", "#E3AC73", 310], ["North America", "#B65856", 383], ["Oceania", "#287E5C", 470], ["South America", "#AC8358", 530]
];
How do I draw the circles dependent on the length of the country names and get the same spacing between the cirlces?
You can draw text element to get bounding box on canvas. Then adjust position based on the last element's width:
svg.selectAll("text")
.data(data).enter()
.append("text")
.attr("x", function(d) {
return x_pos;
})
.attr("y", 50)
.style("display", "none")
.text(function(d) { return d; });
svg.selectAll("text")
.style("display", "block")
.attr("x", function(d) {
var c_pos = x_pos;
x_pos = x_pos + this.getBBox().width + distance;
return c_pos;
});
Full example: https://vida.io/documents/C5CSjbWLhoJ8rQmhF
This is how I would do it.
//This will be your array of legends
var legendItems = []
var legendCount = legendItems.length;
var legendSectionWidth = width / (legendCount+1);
//This will be your "y" value going forward. You can assign anything you want. I have used the following if else case, you should replace it with your logic to calculate y.
var vert = 0;
if(legendPosition == "bottom"){
if(showAxes)
vert = height + legendVerticalPad + containerHeight*0.04;
else
vert = height + legendVerticalPad + containerHeight*0.02;
}
for(var i = 0; i < legendCount; i++){
var text = svg.append('text')
.attr('x', (i+1)*legendSectionWidth)
.attr('y', vert)
.attr('class', 'legend-text '+legendItems[i])
.style('text-anchor', 'middle')
.style('dominant-baseline', 'central')
.text(function() {
return legendItems[i];
});
var len = text[0][0].getComputedTextLength();
// you could use circles here, just change the width n height to rx and x and y to cx and cy// you could use circles here, just change the width n height to rx and x and y to cx and cy`enter code here`
//The value 5 is your choice, i use 5px between my rect and text
svg.append('rect')
.attr('x', (i+1)*legendSectionWidth - len/2 - 5 - legendRectSize)
.attr('y', vert - legendRectSize/2)
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.attr('class', function () { return 'legend '+ legendItems[i];} )
.attr('label', function() {
return legendItems[i];
})
}
The result is something like this
The following images prove that the legends(combo of rect and text) are equi-distant from each and place right in the center of the provided width. And with this logic, no matter what is the no of legends you need, all will be placed equi-distant from each other and show up right in the middle of the screen
I hope this helps.
First off, d refers to an individual, bound data point, while i refers to its index in the dataset. To look at the previous data point, you would need to reference the original dataset, rather than the provided datapoint.
Assuming you had:
var dataset = ["Africa","Asia", "Caribbean", "Central America", "Europe", "Middle East", "North America", "Oceania", "South America"];
d3.select('.foo').data(dataset)....
You would want to change your d[i - 1] references in your position handler to dataset[i - 1]
With that fixed, your first element will still blow up, since it's at dataset[0]. dataset[i - 1] is dataset[-1] in that case.
You could change your return statement to:
return i ? (i * 40 + dataset[i-1].length + 7) : i;

Protovis Scale Interaction Line Chart example

I am following the scale interaction example # http://mbostock.github.com/protovis/docs/invert.html where I am trying to draw 2 line series chart.
My JSON file is as follows:
var psSeriesData =
[{"Dates":["1-10","2-10","3-10","4-10","5-10","6-10","7-10","8-10"],"ScoresForA":
[78.78,79.79,78.78,78.78,78.78,79.79,79.79,76.92],"ScoresForB":
[78.78,79.79,78.78,78.78,78.78,79.79,79.79,76.92]}]
I intend to plot the x-axis using the Dates and the 2 line chart using ScoresForA and ScoresForB respectively but am confused how to do so after much tweaking.
My code is as follows:
var data = pv.range(2).map(function(i) {
return pv.range(0, 10, .1).map(function(x) {
return {x: psSeriesData.Dates, y: psSeriesData.ScoresForA,ScoresForB };
});
});
/* Chart dimensions and scales. */
var w = 400,
h = 200,
x = pv.Scale.linear(0, 9.9).range(0, w),
y = pv.Scale.linear(0, 10).range(0, h),
i = -1;
/* The root panel. */
var vis = new pv.Panel()
.width(w)
.height(h)
.bottom(20)
.left(20)
.right(10)
.top(5);
/* Y-ticks. */
vis.add(pv.Rule)
.data(pv.range(100))
.visible(function() !(this.index % 2))
.bottom(function(d) Math.round(y(d)) - .5)
.strokeStyle(function(d) d ? "#eee" : "#000")
.anchor("left").add(pv.Label)
.text(function(d) (d * 10).toFixed(0) );
/* X-ticks. */
vis.add(pv.Rule)
.data(x.ticks())
.visible(function(d) d > 0)
.left(function(d) Math.round(x(d)) - .5)
.strokeStyle(function(d) d ? "#eee" : "#000")
.anchor("bottom").add(pv.Label)
.text(function(d) d.toFixed());
/* A panel for each data series. */
var panel = vis.add(pv.Panel)
.data(data);
/* The line. */
var line = panel.add(pv.Line)
.data(function(d) d)
.left(function(d) x(d.x))
.bottom(function(d) y(d.y))
.lineWidth(3);
/* The mouseover dots and label. */
line.add(pv.Dot)
.visible(function() i >= 0)
.data(function(d) [d[i]])
.fillStyle(function() line.strokeStyle())
.strokeStyle("#000")
.size(20)
.lineWidth(1)
.add(pv.Dot)
.left(10)
.bottom(function() this.parent.index * 12 + 10)
.anchor("right").add(pv.Label)
.text(function(d) (d.y * 10).toFixed(5));
/* An invisible bar to capture events (without flickering). */
vis.add(pv.Bar)
.fillStyle("rgba(0,0,0,.001)")
.event("mouseout", function() {
i = -1;
return vis;
})
.event("mousemove", function() {
var mx = x.invert(vis.mouse().x);
i = pv.search(data[0].map(function(d) d.x), mx);
i = i < 0 ? (-i - 2) : i;
return vis;
});
vis.render();
What am I doing wrong?
After inputs were given by nrabinowitz:
var psSeriesData = {
"Dates": ["1/10","2/10","3/10","4/10","5/10","6/10","7/10","8/10"],
"ScoresForA": [78.78,79.79,78.78,78.78,78.78,79.79,79.79,76.92],
"ScoresForB": [78.78,79.79,78.78,78.78,78.78,79.79,79.79,76.92]
};
// start by iterating over the two keys for your time series data
var data = ["ScoresForA","ScoresForB"].map(function(seriesKey) {
// use pv.range to walk through the indexes of the
// date array (basically the same as a for loop)
return pv.range(0, psSeriesData.Dates.length)
// map these indexes to an array of objects
.map(function(dateIndex) {
// now return an object with the date index
// and series value for that index
return {
x: dateIndex,
y: psSeriesData[seriesKey][dateIndex]
}
});
});
/* Chart dimensions and scales. */
var w = 400,
h = 200,
x = pv.Scale.linear(0, 9.9).range(0, w),
y = pv.Scale.linear(0, 10).range(0, h),
i = -1;
/* The root panel. */
var vis = new pv.Panel()
.width(w)
.height(h)
.bottom(20)
.left(20)
.right(10)
.top(5);
/* Y-ticks. */
vis.add(pv.Rule)
.data(pv.range(100))
.visible(function() !(this.index % 2))
.bottom(function(d) Math.round(y(d)) - .5)
.strokeStyle(function(d) d ? "#eee" : "#000")
.anchor("left").add(pv.Label)
.text(function(d) (d * 10).toFixed(0) );
/* X-ticks. */
vis.add(pv.Rule)
//.data(function(d) [d[i].Dates])
.data(pv.range(0, psSeriesData.Dates.length).map(function(a) (psSeriesData[a].Dates)))
.visible(function(d) d > 0)
.left(function(d) Math.round(x(d)) - .5)
.strokeStyle(function(d) d ? "#eee" : "#000")
.anchor("bottom").add(pv.Label)
.text(function(d) (d).toFixed());
/* A panel for each data series. */
var panel = vis.add(pv.Panel)
.data(data);
/* The line. */
var line = panel.add(pv.Line)
.data(function(d) d)
.left(function(d) x(d.x))
.bottom(function(d) y(d.y))
.lineWidth(3);
/* The mouseover dots and label. */
line.add(pv.Dot)
.visible(function() i >= 0)
.data(function(d) [d[i]])
.fillStyle(function() line.strokeStyle())
.strokeStyle("#000")
.size(20)
.lineWidth(1)
.add(pv.Dot)
.left(10)
.bottom(function() this.parent.index * 12 + 10)
.anchor("right").add(pv.Label)
.text(function(d) (d.y ).toFixed(5));
/* An invisible bar to capture events (without flickering). */
vis.add(pv.Bar)
.fillStyle("rgba(0,0,0,.001)")
.event("mouseout", function() {
i = -1;
return vis;
})
.event("mousemove", function() {
var mx = x.invert(vis.mouse().x);
i = pv.search(data[0].map(function(d) d.x), mx);
i = i < 0 ? (-i - 2) : i;
return vis;
});
vis.render();
Dates are still not displaying out as the x-axis, even though I used the map function and array referencing. There seems to be a problem reading the 'Dates' property. Any advices
Error: TypeError: Cannot read property 'Dates' of undefined
The first thing to do when working on a visualization like this (and especially when following the Protovis examples) is to make sure your data is in the format you need. I haven't gone through all of your code here, but you've got some clear issues with the data right up front:
Why is your initial data in an array? Is there any reason to include the enclosing straight braces (i.e. the outer brackets in psSeriesData = [{ ... }])? There's no reason for this that I can see in the code as you've presented it, and it's only going to confuse things (e.g. psSeriesData.Dates is undefined - you'd need to reference psSeriesData[0].Dates).
I'm not at all clear on what you're doing in your initial data-setup code, but I'm pretty certain it's not giving you what you want - it looks like a blind cut-and-paste from the example, even though it doesn't apply. The example is using pv.range to generate fake data - you don't need this, you have real data, and you can walk through this instead.
The best way to start here is to understand what the data is supposed to look like. In the example, the data is produced like this:
data = pv.range(3).map(function(i) {
return pv.range(0, 10, .1).map(function(x) {
return {x: x, y: i + Math.sin(x) + Math.random() * .5 + 2};
});
});
Run this in a console, and you'll see that the data produced looks like this:
[
[
{
x: 0.1,
y: 2.34
},
// ...
],
// ...
]
The outer array holds the diffent time series; each time series is an array of objects like {x:0.1, y:2.34}. If your data doesn't look like this, it won't work with the example code.
Your initial data should work fine for this, but you'll need to get it into the right format. One issue here is the list of dates - these are strings, not numbers, and you won't be able to use them as data unless you either convert them to Date objects (this is a real pain, avoid it if possible) or map them to numbers - the latter is easy here because they're in a regular series. (If you had unevenly spaced dates, this all would be more complex, but let's forget that for now.) You can just use the index of the dates as the x values, and then use your two series as the y values.
Putting this all together, you can format your data like this:
// note - no enclosing array
var psSeriesData = {
"Dates": ["1-10","2-10","3-10","4-10","5-10","6-10","7-10", "8-10"],
"ScoresForA": [78.78,79.79,78.78,78.78,78.78,79.79,79.79,76.92],
"ScoresForB": [78.78,79.79,78.78,78.78,78.78,79.79,79.79,76.92]
};
// start by iterating over the two keys for your time series data
var data = ["ScoresForA","ScoresForB"].map(function(seriesKey) {
// use pv.range to walk through the indexes of the
// date array (basically the same as a for loop)
return pv.range(0, psSeriesData.Dates.length)
// map these indexes to an array of objects
.map(function(dateIndex) {
// now return an object with the date index
// and series value for that index
return {
x: dateIndex,
y: psSeriesData[seriesKey][dateIndex]
}
});
});
There are a lot of other ways to do this as well, but the main point is that you come out with an array like this: [[{x:0, y:79.79}, ...], ...]. I haven't looked at the rest of your code, but now that your data is in the correct format, you should be able to replace the fake data in the example with the real data in your code, and have the whole thing work as expected (though you'll need to change any assumptions in the example about the expected max and min values for x and y).

Categories