I'm learning d3.js and need to create a visualization mirroring that of the example image I've included in the below fiddle.
http://jsfiddle.net/mike_ellan/37PYJ/
I know how to create and bind text elements and was planning on doing so for the labeling inside the circles but I'm having a hard time getting my head around how I should be doing it with this particular data structure. I don't have the option to request a flatter json structure so I have to get it mapped out properly as is. Here is the data I'm starting with:
var consumption = [
{
"fruits": [
{
"year":"2011",
"apples":"213",
"oranges":"176",
"pears":"987"
},
{
"year":"2012",
"apples":"199",
"oranges":"234",
"pears":"672"
}
]
}
];
As for the years, my plan was to generate a 2 row/column table and then add a column for each year based on whats coming back in the json. Can you give me some tips or guidance on how I might achieve this?
Mike this is a quick way to make the graph.
Create the Columns and append header
// Group Columns
var columns = groupColumn.selectAll("g")
.data(consumption[0].fruits)
.enter()
.append("g")
.attr("class", function(d,i) { return "column - " + i });
// Append year
columns.append("text")
.attr("transform", function(d,i) { return "translate(" + i*(width/2) + "," + height/2 + ")"; })
.text(function(d) {return d.year });
Create rows and append fields
// Append row
var groupRow = columns.append("g")
.attr("class", "fruits")
.attr("transform", function(d,i) { return "translate(" + i*(width/2) + "," + height/2 + ")"; })
// Append fields of row
var rows = groupRow.selectAll("circle")
.data(function(d) {return d3.entries(d).filter(function(d) { return d.key !== "year"; })})
.enter()
.append("circle")
.attr("cx",function(d,i){ return (60 * i) + 30 } )
.attr("cy",50)
.attr("r",25)
.attr("fill", function(d){
if(d.key === "apples"){ return "red" }
else if (d.key === "oranges"){ return "orange" }
else if (d.key === "pears"){ return "green" }
})
Append Text to Fields
var text = groupRow.selectAll("text")
.data(function(d) {return d3.entries(d).filter(function(d) { return d.key !== "year"; })})
.enter()
.append("text")
.attr("dx", function(d,i){ return (60 * i) + 20 })
.attr("dy", 55)
.attr("fill", "white")
.text(function(d) {return d.value});
View code http://jsfiddle.net/jmeza/37PYJ/3/
The example is not complete, I give you the first steps...
Related
I have this chart:
The symbol names, '$TSLA' and '$AAPL' are not appended to the line, they are simply placed there with the appropriate x and y values. I would like to append them to the line, such that if the line ended at a different position, the symbol name would appear next to it. Like in this example.
Here is the code:
var svg = d3.select('#graph')
.append('svg')
.attr('width', w)
.attr('height', h);
svg.append('path')
.datum(data)
.attr('class', 'stock')
.attr('stroke', '#157145') ....
//I have included the above code because it is what's different from the link-
//my lines are appended to the variable 'svg'. I am, however, selecting the correct class
//in the below code:
....
var sec = d3.selectAll(".stock")
.data(kvals) //this data follows the same pattern as the examples'
.enter().append("g")
.attr("class", "stock");
sec.append("text")
.datum(function(d){
return {
name: d.name,
value: d.values[d.values.length-1]
};
})
.attr("transform", function(d){
console.log(xScale(d.value.date)); //not displayed in console.
return "translate(" + xScale(d.value.date) + "," + yScale(d.value.stock) +")"
})
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d){
return d.name;
});
Thank you for your help. If you need all the code, I can paste it.
Here's the code I used to add the labels. First part (knames) is what I used to plot the line too. (there might be an easier way of doing this):
var knames = d3.scaleOrdinal();
knames.domain(Object.keys(data[0]).filter(function(key){ //only get non-sma values
return !key.match(/([_])|(d)|(band)\w+/g)
}));
var kvals = knames.domain().map(function(name){
return {
name: name,
values: data.map(function(d){
return {
date: d.date,
stock: d[name]
};
})
};
});
var sec = svg.selectAll("stock")
.data(kvals)
.enter().append("g")
.attr("class", "stock");
sec.append("text")
.datum(function(d){
return {
name: d.name,
value: d.values[d.values.length-1]
};
})
.attr("transform", function(d){
return "translate(" + xScale(d.value.date) + "," + yScale(d.value.stock) +")"
})
.attr("x", 7)
.attr("dy", ".3em")
.style("fill", function(d) {
return accent(d.name);
})
.style('font-family', 'Helvetica')
.style('font-size', '11px')
.style('letter-spacing', '1px')
.style('text-transform', 'uppercase')
.text(function(d){
return d.name;
});
[Sorry the title was quite badly formulated. I would change it if I could.]
I'm searching for a way to append text elements from a array or arrays in the data.
EDIT: I can already do a 1 level enter .data(mydata).enter(). What I'm trying here is a second level of enter. Like if mydata was an object which contained an array mydata.sourceLinks.
cf. the coments in this small code snippet:
var c = svg.append("g")
.selectAll(".node")
.data(d.nodes)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", function(i) {
return "translate(" + i.x + "," + i.y + ")"
})
c.append("text")
.attr("x", -200)
.attr("y", 30)
.attr("text-anchor", "start")
.attr("font-size","10px")
.text(function(d){
// d.sourceLinks is an array of elements
// console.log(d.sourceLinks[0].target.name);
// Here I would like to apped('text') for each of the elements in the array
// and write d.sourceLinks[i].target.name in this <text>
})
;
I tried a lot of different things with .data(d).enter() but it never worked and I got lot's of errors.
I also tried to insert html instead of text where I could insert linebreaks (that's ultimately what I'm trying to achieve).
I also tried
c.append("foreignobject")
.filter(function(i) { // left nodes
return i.x < width / 2;
})
.attr('class','sublabel')
.attr("x", -200)
.attr("y", 30)
.attr("width", 200)
.attr("height", 200)
.append("body")
.attr("xmlns","http://www.w3.org/1999/xhtml")
.append("div");
but this never showed up anywhere in my page.
Your question was not exactly clear, until I see your comment. So, if you want to deal with data that is an array of arrays, you can have several "enter" selections in nested elements, since the child inherits the data from the parent.
Suppose that we have this array of arrays:
var data = [
["colours", "green", "blue"],
["shapes", "square", "triangle"],
["languages", "javascript", "c++"]
];
We will bind the data to groups, as you did. Then, for each group, we will bind the individual array to the text elements. That's the important thing in the data function:
.data(d => d)
That makes the child selection receiving an individual array of the parent selection.
Check the snippet:
var data = [
["colours", "green", "blue"],
["shapes", "square", "triangle"],
["languages", "javascript", "c++"]
];
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var groups = svg.selectAll("groups")
.data(data)
.enter()
.append("g")
.attr("transform", (d, i) => "translate(" + (50 + i * 100) + ",0)");
var texts = groups.selectAll("texts")
.data(d => d)
.enter()
.append("text")
.attr("y", (d, i) => 10 + i * 20)
.text(d => d);
<script src="https://d3js.org/d3.v4.min.js"></script>
Now, regarding your code. if d.nodes is an array of arrays, these are the changes:
var c = svg.append("g")
.selectAll(".node")
.data(d.nodes)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", function(i) {
return "translate(" + i.x + "," + i.y + ")"
});//this selection remains the same
var myTexts = c.selectAll("myText")//a new selection using 'c'
.data(function(d){ return d;})//we bind each inner array
.enter()//we have a nested enter selection
.append("text")
.attr("x", -200)
.attr("y", 30)
.attr("text-anchor", "start")
.attr("font-size", "10px")
.text(function(d) {
return d;//change here according to your needs
});
You should use enter like this :
var data = ["aaa", "abc", "abd"];
var svg = d3.select("body").append("svg")
.attr("width", 200)
.attr("height", 200);
svg.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x", function(d,i) {
return 20 + 50 * i;
})
.attr("y", 100)
.text(function(d) { return d; });
See this fiddle : https://jsfiddle.net/t3eyqu7z/
i wanted to know is there way to distinguish between which element of the parent array we are appending onto and append selectively?
eg:
cell = parent.selectAll("g")
.data(nodes)
.enter()
.append("svg:g")
.filter(function(d,j){ var a = parent.selectAll("text");
for(i=0;i<3;i++)
if(a[i][0].innerHTML == d.parent.name)
return true;
return false;
})
.attr("class", "cell")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.on("click", function(d) { return zoom(node == d.parent ? root : d.parent); });
parent is cell of the previous iteration which consists of 3 cells
i want to writ a filter logic such that i can choose which object of nodes array goes to which cell
i can't hard code it as it will be dynamic>
thanks for you help.
I am trying to work on a project where I need to generate rows of grouped data and draw dots on based on Json data from two different files.
I need to place a series of dots initially and then add another series later upon a button push. For testing purposes I have two Json files: one for sales and one for Buys. Each file has two customers with nested data for sales or buys. I group by Company, drawing a red dot for each sale, with this code. This works very well:
function loadSVG() {
//Load in GeoJSON data
//d3.json("data/zzMonthlySalesAndBuysComb.json", function (json) {
d3.json("data/zzMonthlySales2.json", function (json) {
g = svg.append('g').classed("chart", true)
.attr("width", w)
.selectAll(".csMove")
.data(json, function (d) { return d.CompanyName + d.Company; })
.enter()
.append("g")
.classed("csMove", true)
//.attr({ width: w, height: 100 })
//.attr("transform", function (d, i) {
//return "translate(0," + h / 2 + ")";
//})
.attr("transform", function (d, i) { return "translate(0, " + i * 100 + ")"; })
.append("g")
.classed("CustomerBox", true);
//This test code
g.append("rect")
.attr("width", w)
.attr("height", function (d) { return h / 2; })
.style("fill", "silver");
var SalesDot = svg.selectAll(".CustomerBox").selectAll(".Sdot")
//.data(json)
.data(function (d) { return d.monthlySales })
.enter()
.append("g")
.classed("Sdot", true);
//then we add the circles in the correct company group
SalesDot
.append("circle")
.attr("cx", function (d) { return ((d.month - 20130001) / 2); })
.attr("cy", function (d) { return d.sales })
.attr("r", 5)
.style("fill", "red");
//Test - add dots initially
});
}
This works great. but this is where it fails. I have a button on the page and when I press the button I run this function which will load the buys data, I just get just two green dots each at the 0, 0 coordinates of the two groups.
function AddToSVG() {
//Load in GeoJSON data
//d3.json("data/zzMonthlyBuys2.json", function (json2) {
d3.json("data/zzMonthlyBuys2.json", function (json2) {
//add Green Circles.
var BuysDot = svg.selectAll(".CustomerBox").selectAll(".Bdot")
.data(json2)
//.data(function (d) { return d.monthlySales })
.enter()
.append("g")
.classed("Bdot", true);
//then we add the circles in the correct company group
BuysDot
.data(function (d) {
return d.monthlyBuys;
})
.enter()
.append("circle")
.attr("cx", function (d) {
return ((d.monthBuys - 20130001) / 2);
})
.attr("cy", function (d) { return d.buys })
.attr("r", 5)
.style("fill", "green");
});
}
Specifically what is happening is that the system still sees d as having data from monthlySales rather than MonthlyBuys. I see this when I put a break point at return d.monthlyBuys.
Does anyone know how I can fix this? I need the Buys and MonthlyBuys to be drawn over the existing groups for the correct Customers.
I am trying to implement an extended version of the Grouped Bar Chart. Everything works fine, except updating the plot. In a regular bar chart, I would do something like this:
function draw(data) {
// Join the data with the selection
var bars = svg.selectAll(".bar")
.data(data);
// Create new bars if needed
bars.enter().append("rect")
...
// Update existing bars if needed
bars.transition()
...
// Remove bars if needed
bars.exit().remove();
}
Works like a charm. Now i tried the same with my groups:
var groups = chart.selectAll(".group")
.data(data);
groups.enter().append("g")
.attr("class", "group")
.attr("transform", function (d) {
return "translate(" + 0 + "," + y(d.name) + ")";
});
var bars = groups.selectAll(".bar").data(function (d) {
return d.values;
});
bars.enter().append("rect")
...
groups.transition().duration(750).attr("transform", function (d) {
return "translate(" + 0 + "," + y(d.name) + ")";
});
groups.exit().remove();
But: it only appends more and more groups on every update, no transitions or exits.
I am aware of this question: D3: update data with multiple elements in a group. However, I think did the setup just as described in the answer, without success.
EDIT: I am still trying to figure this out. I updated the fiddles.
Here is the working example without groups: http://jsfiddle.net/w2q0kjgd/2/
This is the not working example with groups: http://jsfiddle.net/o3fpaz2d/9/
I know it has been almost a month since the original posting, I tried to do something else in between and then have a look at this problem again, sometimes this helps. But I just cannot figure it out...
Please update the group before you select all the bars..
var groups = chart.selectAll(".group")
.data(data);
groups.enter()...
groups.transition().duration(750).attr("transform", function (d) {
return "translate(" + 0 + "," + y(d.name) + ")";
});
var bars = groups.selectAll(".bar").data(function (d) {
return d.values;
});
//add bars
//update bars
bars.exit().remove();
groups.exit().remove();
I was also playing around with the same block and was trying to figure it out. In the end, this solution worked for me:
var bars = g
.selectAll("g")
.data(data, function(d) {return d ? d.format_date : this.id; });
var enter = bars
.enter().append("g");
bars = enter.merge(bars)
.attr("transform", function(d) { return "translate(" + x0(d.format_date) + ",0)"; });
var rect = bars
.selectAll("rect")
.data(function(d) { return keys.map(function(key) { return {key: key, value: d[key]}; }); })
rect
.enter().append("rect").merge(rect)
.attr("x", function(d) { return x1(d.key); })
.attr("y", function(d) { return y(d.value); })
.attr("width", x1.bandwidth())
.attr("height", function(d) { return height - y(d.value); })
.attr("fill", function(d) { return z(d.key); });
I think the trick here is that you need to update and merge it twice. One is for your x0 axes overall group and the other is for the bars within that group. Hope this also works out for you.