D3 Data join with containers - javascript

How do I properly do data joins with wrapper elements on some of the levels? For instance, say I have the data [[a,b],[x,y]] and I want to transform it to this:
<wrapper1>
<content1>
<wrapper2>
<content2>a</content2>
</wrapper2>
<wrapper2>
<content2>b</content2>
</wrapper2>
</content1>
</wrapper1>
<wrapper1>
<content1>
<wrapper2>
<content2>x</content2>
</wrapper2>
<wrapper2>
<content2>y</content2>
</wrapper2>
</content1>
</wrapper1>
Is there a standard way to create elements with this structure?

Here is a code that creates that exact tree structure.
The outerSelection uses as data the outer array, which has 2 elements: [a, b] and [x, y].
Then, the innerSelection uses as data the inner arrays, each one containing two elements: "a" and "b" for the first one and "x" and "y" for the second one.
Click "Run conde snippet" and look at your console (the console of your browser, which you can expand, not the snippet's one):
var body = d3.select("body");
var data = [
["a", "b"],
["x", "y"]
];
var outerSelection = body.selectAll(null)
.data(data)
.enter()
.append("wrapper1")
.append("content1");
var innerSelection = outerSelection.selectAll(null)
.data(function(d) {
return d
})
.enter()
.append("wrapper2")
.append("content2")
.html(function(d) {
return d
})
console.log(body.node())
<script src="https://d3js.org/d3.v4.min.js"></script>

You can achieve this by using the d3 'data' function. As you have two levels of data here, this is how I would do it:
data = [['a','b'],['x','y']];
d3.select("#div1")
.selectAll("wrapper1")
.data(data)
.enter().append("wrapper1")
.attr("d", function (d) {
d3.select(this)
.selectAll("content1")
.data(d)
.enter().append("content1")
.attr("d", function (d2) {
d3.select(this)
.append("wrapper2")
.append("content2")
.html(d2);
})
});
JSFiddle

Related

Assign specific color to specific segment in D3 Pie Chart

I have built a pie/doughnut chart using D3js (v4) as an Ember component and I am trying to have segments with specific labels be filled with a specific color but it is proving difficult.
To color the charts I have the following code:
marc = arc().outerRadius(radius - 10).innerRadius(radius - donutwidth),
color = scaleOrdinal().range(['#49b6d6', '#f59c1a', '#ff5b57', '#00acac',]),
gEnter.append("path")
.attr("d", marc)
.attr("fill", (d, i) => {
return color(i);
})
The above works fine and fills the arcs with the selected colors but not the color I want per arc. The index of the array is consistent so I tried to simply re-arrange the order of the colors with no effect.
I also tried using an if statement based on the index like:
gEnter.append("path")
.attr("d", marc)
.attr("fill", (d, i) => {
if (i === 0 { return color([0]) }
})
This does fill in the segment which is index 0 but not with the selected color from the list. Changing the number in color([0]) actually produces no change at all. This is also true if I try to use a conditional based on the string of the Label instead of the index of the array.
EDIT
As part of the Ember Computed Property that formats data for the chart, the data is re-ordered so that each label is presented in the same order every time. THe computed property is as follows:
//takes the ember model 'referralsource' and groups it as needed
sourceData: groupBy('referralsource', 'label'),
//ember computed property that provides data to chart
pieData: Ember.computed('sourceData', function() {
let objs = this.get('sourceData')
let sortedObjs = _.sortBy(objs, 'value')
let newArray = []
sortedObjs.forEach(function(x) {
let newLabel = x.value
let count = x.items.length
let newData = {
label: newLabel,
count: count
}
newArray.push(newData)
})
return newArray
}),
in your first example, try changing this:
color = scaleOrdinal().range(['#49b6d6', '#f59c1a', '#ff5b57', '#00acac',]),
for this:
color = scaleOrdinal().range([0,4]),
color.domain(['#49b6d6', '#f59c1a', '#ff5b57', '#00acac']),
the range you use to indicate the size of your scale (in this case 4 because you put 4 colors) and the domain specifies what things are in each position of that scale
If your labels are the same each time (or draw from the same pool of options), you can specify a specific domain. In an ordinal scale, the domain :
sets the domain to the specified array of values. The first element in
domain will be mapped to the first element in the range, the second
domain value to the second range value, and so on (from the API documentation).
By setting the domain equal to an array that contains each possible label option, you can easily assign a color to each label. The example below has five possible labels, the first row uses the opposite data array order as the second row, the third row uses a random order with duplicates. All three rows associate each datum with a specific color consistently:
var labels = ["redData","blueData","orangeData","pinkData","greenData"];
var colors = ["crimson","steelblue","orange","lightsalmon","lawngreen"];
var scale = d3.scaleOrdinal()
.domain(labels) // input values
.range(colors); // output values
var svg = d3.select("svg");
// initial order
svg.selectAll(null)
.data(labels)
.enter()
.append("circle")
.attr("cy",40)
.attr("cx", function(d,i) { return i * 40+ 20; })
.attr("r",15)
.attr("fill",function(d) { return scale(d); });
// reverse order
svg.selectAll(null)
.data(labels.reverse())
.enter()
.append("circle")
.attr("cy",80)
.attr("cx", function(d,i) { return i * 40+ 20; })
.attr("r",15)
.attr("fill",function(d) { return scale(d); });
// random labels
svg.selectAll(null)
.data(["blueData","blueData","redData","orangeData","blueData"])
.enter()
.append("circle")
.attr("cy",120)
.attr("cx", function(d,i) { return i * 40+ 20; })
.attr("r",15)
.attr("fill",function(d) { return scale(d); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="600" height="400"></svg>

d3.js build connections by using loop function

I have build a connection by using d3. The codes show the data and method of connection:
var places = {
TYO: [139.76, 35.68],
BKK: [100.48, 13.75],
BER: [13.40, 52.52],
NYC: [-74.00, 40.71],
};
var connections = {
CONN1: [places.TYO, places.BKK],
CONN2: [places.BER, places.NYC],
};
...
svg.append("path")
.datum({type: "LineString", coordinates: connections.CONN1})
.attr("class", "route")
.attr("d", path);
svg.append("path")
.datum({type: "LineString", coordinates: connections.CONN2})
.attr("class", "route")
.attr("d", path);
You can see my codes, that I use the two identical methods to build two connections. That is not good to build more connections.
I am wondering, if there is a loop function to interpret the connections by using data "connections" directly? I mean, I could get information for data "connections" and use them directly to build connections.
I have thought some ways, such as .datum({type: "LineString", function(d,i) {
return coordinates: connections[i];});. But it does not work.
Could you please tell me some way to solve it? Thanks.
Generally when you want to append many features in d3, you want to use an array not an object. With an array you can use a d3 enter selection which will then allow you to build as many features as you need (if sticking to an object, note that connections[0] is not what you are looking for, connections["conn1"] is).
Instead, use a data structure like:
var connections = [
[places.TYO, places.NYC],
[places.BKK, places.BER],
...
]
If you must have identifying or other properties for each datapoint use something like:
var connections = [
{points:[places.TYO, places.NYC],id: 1,...},
{points:[places.BKK, places.BER],id: 2,...},
...
]
For these set ups you can build your lines as follows:
paths = svg.selectAll(".connection")
.data(connections)
.enter()
.append("path")
.attr("class","connection")
.attr('d', function(d) {
return path ({
type:"LineString",
coordinates: d
});
})
See here. Or:
paths = svg.selectAll(".connection")
.data(connections)
.enter()
.append("path")
.attr("class","connection")
.attr('d', function(d) {
return path ({
type:"LineString",
coordinates: d.points
});
})
Alternatively, you can use a data set up like:
var connections = [
{target:"TYO", source:"NYC"},
{target:"BKK", source: "BER"},
...
]
paths = svg.selectAll(".connection")
.data(connections)
.enter()
.append("path")
.attr("class","connection")
.attr('d', function(d) {
return path ({
type:"LineString",
coordinates: [ places[d.source],places[d.target] ]
});
})
See here.
If selecting elements that don't yet exist, using these lines
d3.select("...")
.data(data)
.enter()
.append("path")
will append a path for each item in the data array - this means that d3 generally avoids the use of for loops as the desired behavior is baked right into d3 itself.

Create Path Between Initial Child Nodes - D3 Tree Layout

I am using the D3 Collapsible Tree Layout and would like to create a path that begins at the first child node and travels all the way to the last initial child node, while mainlining it's position when the graph is scaled/updated. Basically this is what I'm going for
I've tried this so far:
var nodes = tree.nodes(root).reverse();
/**
* transition logic, etc
**/
var rootNodes = [ nodes[0], nodes[1] ]; //will use first 2 nodes as an example (will want to include all initial root nodes)
var lineFunction = d3.svg.line('step')
.x(function(d) { return d.y; })
.y(function(d) { return d.x; })
.interpolate("linear");
var lineGraph = vis.append("path")
.attr("d", lineFunction(nodes))
.attr("stroke", "#ccc")
.attr("stroke-width", "1.5")
.attr("fill", "none");
}
However I'm not sure how to get it to transition within the update() function properly, also it creates extra paths under the root nodes and ends up looking like this:
Any ideas?
UPDATE: If I grab the root of my data array, I can keep my new paths within the first child nodes of my tree
root = json;//JSON Object returned from my program
//...
var rootNodes = [ root.children[0], root.children[1] ];

Obtaining data bound to dom elements

I am currently learning data visualizations with d3.js. I am using the tutorial on the d3.js site. I am at the part where the data bound to DOM elements have to retrieved. I did it exactly as they have shown, but I am not able to get the data from it.
Here is the code from the beginning:
var theData=[1,2,3]
var p= d3.select("body")
.selectAll("p")
.data(theData)
.enter()
.append("p")
.text("hello")
This displays:
hello
hello
hello
Now, on the site, it tells me to enter the following code to obtain the data bound i.e. 1,2 & 3.
var theData=[1,2,3]
var p= d3.select("body")
.selectAll("p")
.data(theData)
.enter()
.append("p")
.text(function (d) { return d; } )
Even after doing this, the page does not change,like it should to:
1
2
3
It remains the same(it keeps showing the three hello's).
How do I get the data back?
when just using this :
var theData2 = [ 1, 2, 3 ]
var p2 = d3.select("body").selectAll("p")
.data(theData2)
.enter()
.append("p")
.text( function (d) {
console.log(d);
return d; } );
It works, you're using variables with the same names which you really shouldn't do
http://jsfiddle.net/cwqwbw3u/1/
When I try your code, it works (however, I am a all for pretty javascript, so I added ';' when approriate).
I would suggest checking out the console on the browsers developers tools (perhaps there are showing errors already) and change your code to something like this:
var theData=[1,2,3];
var p= d3.select("body")
.selectAll("p")
.data(theData)
.enter()
.append("p")
.text(function (d) {
console.log("checking d");
console.log(d);
return d;
} );
it would be really weird if you did not see at least "checking d" in your console...
When our dataset contains more items than available DOM elements, the surplus data items are stored in a sub set of this selection called the enter selection. When you try to use enter for the second time, there are already 3 elements and so no items are stored in enter selection. You should have used the same enter selection for both tasks.
var theData=[1,2,3]
var p = d3.select("body")
.selectAll("p")
.data(theData);
p.enter()
.append("p")
.text("hello");
p.enter()
.append("p")
.text(function (d) { return d; } );
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

multiple circles / lines using d3.js

I have a d3.js problem and have struggled with this for a while and just can not seem to solve it. I believe it is pretty easy, but am missing something very basic.
Specifically, I have the following code, which generates a line and 2 circles for the 1st entry in the JSON - I have 'hardcoded' it for the first entry.
I'd like to now add the 2nd and 3rd entries of the JSON file to the graph and have control over line and circle colors and then generalize the code.
From reading the documentation and StackOverflow, it seems like the proper approach is to use nesting, but I can't seem to make it work?
The code is on jsfiddle at the following URL and the javascript is below.
http://jsfiddle.net/GVmVk/
// INPUT
dataset2 =
[
{
movie : "test",
results :
[
{ week: "20130101", revenue: "60"},
{ week: "20130201", revenue: "80"}
]
},
{
movie : "beta",
results :
[
{ week: "20130101", revenue: "40"},
{ week: "20130201", revenue: "50"}
]
},
{
movie : "gamm",
results :
[
{ week: "20130101", revenue: "10"},
{ week: "20130201", revenue: "20"}
]
}
];
console.log("1");
var parseDate = d3.time.format("%Y%m%d").parse;
var lineFunction = d3.svg.line()
.x(function(d) { return xScale(parseDate(String(d.week))); })
.y(function(d) { return yScale(d.revenue); })
.interpolate("linear");
console.log("2");
//SVG Width and height
var w = 750;
var h = 250;
//X SCALE AND AXIS STUFF
//var xMin = 0;
//var xMax = 1000;
var xScale = d3.time.scale()
.domain([parseDate("20130101"),parseDate("20131231")])
.range([0, w]);
console.log(parseDate("20130101"));
console.log("3");
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
console.log("4S");
//Y SCALE AND AXIS STUFF
var yScale = d3.scale.linear()
.domain([0, 100])
.range([h, 0]);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.ticks(5);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
console.log("4S1");
//CREATE X-AXIS
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - 30) + ")")
.call(xAxis);
//Create Y axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + 25 + ",0)")
.call(yAxis);
svg.selectAll("circle")
.data(dataset2[0].results)
.enter()
.append("circle")
.attr("cx", function(d) {
// console.log(d[0]);
console.log(parseDate(d.week));
return xScale(parseDate(d.week));
})
.attr("cy", function (d) {
return yScale(d.revenue);
})
.attr("r", 3);
//create line
var lineGraph = svg.append("path")
.attr("d", lineFunction(dataset2[0].results))
.attr("class", "line");
The word "nesting" comes up in two contexts in d3 -- creating nested data arrays with d3.nest, and using nested data to create nested selections.
Your data is already in the correct format for a nested selection -- an array of objects, each of which has a sub-array of individual data points. So you don't need to worry about manipulating the data, you just need to go straight to joining your data to your elements in nested d3 selections:
I'm going to take you through it quickly, but the following tutorials will be good reference for the future:
Thinking with Joins
Nested Selections
How Selections Work
On to your example: you have a top-level data structure that is an array of movie objects, each of which contains a sub-array of weekly revenue values. The first thing you need to decide is what type of elements you want associated with each level of data. You're drawing a line and a set of circles for the data in the sub-array, but aren't currently adding anything for the top-level array objects (the movies). You need to add something for them in order for nested selections to work, and it needs to be something that can contain your line and circle. In SVG, that's almost always going to be a <g> (grouping) element.
To efficiently create one <g> element for every object in your data array -- and to attach the data objects to the elements for future reference -- you create an empty selection, join your data to it, then use the enter() method of the data join selection to add elements for each data object that didn't match an element. In this case, since we don't have any elements to start, all the data objects will be in the enter() selection. However, the same pattern also works when updating some of the data.
var movies = svg //start with your svg selection,
//it will become the parent to the entering <g> elements
.selectAll("g.movie") //select all <g> elements with class "movie"
//that are children of the <svg> element
//contained in the `svg` selection
//this selection will currently be empty
.data( dataset2 ); //join the selection to a data array
//each object in the array will be associated with
//an element in the selection, if those elements exist
//This data-joined selection is now saved as `movies`
movies.enter() //create a selection for the data objects that didn't match elements
.append("g") //add a new <g> element for each data object
.attr("class", "movie") //set it's class to match our selection criteria
//for each movie group, we're going to add *one* line (<path> element),
//and then a create subselection for the circles
.append("path") //add a <path> within *each* new movie <g> element
//the path will *inherit* the data from the <g> element
.attr("class", "line"); //set the class for your CSS
var lineGraph = movies.select("path.line")
//All the entered elements are now available within the movies selection
//(along with any existing elements that we were updating).
//Using select("path") selects the first (and only) path within the group
//regardless of whether we just created it or are updating it.
.attr("d", function(d){ return lineFunction(d.results); });
//the "d" attribute of a path describes its shape;
//the lineFunction creates a "d" definition based on a data array.
//If the data object attached to the path had *only* been the d.results array
//we could have just done .attr("d", lineFunction), since d3
//automatically passes the data object to any function given as the value
//of an .attr(name, value) command. Instead, we needed to create an
//anonymous function to accept the data object and extract the sub-array.
var circles = movies.selectAll("circle")
//there will be multiple circles for each movie group, so we need a
//sub-selection, created with `.selectAll`.
//again, this selection will initially be empty.
.data( function(d) {return d.results; });
//for the circles, we define the data as a function
//The function will be called once for each *movie* element,
//and passed the movie element's data object.
//The resulting array will be assigned to circles within that particular
//movie element (or to an `enter()` selection, if the circles don't exist).
circles.enter() //get the data objects that don't have matching <circle> elements
.append("circle") //create a circle for each
//the circles will be added to the appropriate "g.movie"
//because of the nested selection structure
.attr("r", 3); //the radius doesn't depend on the data,
//so you can set it here, when the circle is created,
//the same as you would set a class.
circles //for attributes that depend on the data, they are set on the entire
//selection (including updating elements), after having created the
//newly entered circles.
.attr("cx", function(d) { return xScale( parseDate(d.week) ); })
.attr("cy", function(d) { return yScale( d.revenue ); });
Live version with the rest of your code: http://jsfiddle.net/GVmVk/3/
You'll need to adjust the domain of your x-scale so that the first data points aren't cut off, and you'll need to decide how you want to use your movie title property, but that should get you going.
Yes indeed, nested selection are the way to go for the circles, although you don't need them for the paths:
svg.selectAll("g.circle")
.data(dataset2)
.enter()
.append("g")
.attr("class", "circle")
.selectAll("circle")
.data(function(d) { return d.results; })
.enter()
.append("circle")
.attr("cx", function(d) {
// console.log(d[0]);
console.log(parseDate(d.week));
return xScale(parseDate(d.week));
})
.attr("cy", function (d) {
return yScale(d.revenue);
})
.attr("r", 3);
//create line
var lineGraph = svg.selectAll("path.line")
.data(dataset2).enter().append("path")
.attr("d", function(d) { return lineFunction(d.results); })
.attr("class", "line");
Complete example here.

Categories

Resources