Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

d3.js: Access data nested 2 level down

Data structure:

var data = [ 
   {name: "male",
   values: [
     { count: 12345,
       date: Date 2015-xxx,
       name: "male" },
     {...}
   ]
  },
  {name: "female",
   values: [
     { count: 6789,
       date: Date 2015-xxx,
       name: "female" },
     {...}
   ]
  }
]

The values that I want to access are data[a].values[b].count

The values are used to draw circles for my plot

code for circle plot:

focus.selectAll(".dot")
    .data(data)
    .enter().append("circle")
    .attr("class", "dot")
    .attr("cx", function(d,i) { return x(d.values[i].date); })
    .attr("cy", function(d,i) { return y(d.values[i].count); })
    .attr("r", 4)
    .style("fill", function(d,i) { return color(d.values[i].name); })

The problem is that i = 1 because of its position in the object.

What I want to do is to loop through all the objects under values. How can I do that?

Edit: I wish to learn how to do it without altering the data, to improve on my skills.

Thanks.

like image 782
shawnngtq Avatar asked Aug 21 '16 00:08

shawnngtq


2 Answers

There are several ways to do what you want using D3 only, without any other library and without altering the data. One of them is using groups to deal with "higher" levels of data (regarding the nested data). Let's see it in this code:

First, I mocked up a dataset just like yours:

var data = [ 
    {name: "male",
    values: [{ x: 123,y: 234},
        { x: 432,y: 221},
        { x: 199,y: 56}]
    },
    {name: "female",
    values: [{ x: 223,y: 111},
        { x: 67,y: 288},
        { x: 19, y: 387}]
    }
];

This is the data we're gonna use. I'm gonna make a scatter plot here (just as an example), so, let's set the domains for the scales accessing the second level of data (x and y inside values):

var xScale = d3.scaleLinear().range([20, 380])
    .domain([0, d3.max(data, function(d){
        return d3.max(d.values, function(d){
            return d.x;
        })
})]);

var yScale = d3.scaleLinear().range([20, 380])
    .domain([0, d3.max(data, function(d){
        return d3.max(d.values, function(d){
            return d.y;
        })
})]);

Now comes the most important part: we're gonna bind the data to "groups", not to the circle elements:

var circlesGroups = svg.selectAll(".circlesGroups")
    .data(data)
    .enter()
    .append("g")
    .attr("fill", function(d){ return (d.name == "male") ? "blue" : "red"});

Once in the first level of data we have 2 objects, D3 will create 2 groups for us.

I also used the groups to set the colours of the circles: if name is "male", the circle is blue, otherwise it's red:

.attr("fill", function(d){ return (d.name == "male") ? "blue" : "red"});

Now, with the groups created, we create circles according to the values in the data of each group, binding the data as follows:

    var circles = circlesGroups.selectAll(".circles")
        .data(function(d){ return d.values})
        .enter()
        .append("circle");

Here, function(d){ return d.values} will bind the data to the circles according to the objects inside values arrays.

And then you position your circles. This is the whole code, click "run code snippet" to see it:

var data = [ 
    {name: "male",
    values: [{ x: 123,y: 234},
        { x: 432,y: 221},
        { x: 199,y: 56}]
    },
    {name: "female",
    values: [{ x: 223,y: 111},
        { x: 67,y: 288},
        { x: 19, y: 387}]
    }
];

var xScale = d3.scaleLinear().range([20, 380])
	.domain([0, d3.max(data, function(d){
			return d3.max(d.values, function(d){
				return d.x;
		})
	})]);
		


var yScale = d3.scaleLinear().range([20, 380])
    .domain([0, d3.max(data, function(d){
			return d3.max(d.values, function(d){
				return d.y;
		})
	})]);

var xAxis = d3.axisBottom(xScale).tickSizeInner(-360);
var yAxis = d3.axisLeft(yScale).tickSizeInner(-360);

var svg = d3.select("body")
	.append("svg")
	.attr("width", 400)
	.attr("height", 400);

svg.append("g")
	.attr("class", "x axis")
	.attr("transform", "translate(0,380)")
	.call(xAxis);
	
svg.append("g")
	.attr("class", "y axis")
	.attr("transform", "translate(20,0)")
	.call(yAxis);
	
var circlesGroups = svg.selectAll(".circlesGroups")
	.data(data)
	.enter()
	.append("g")
	.attr("fill", function(d){ return (d.name == "male") ? "blue" : "red"});
	
var circles = circlesGroups.selectAll(".circles")
	.data(function(d){ return d.values})
	.enter()
	.append("circle");
		
circles.attr("r", 10)
	.attr("cx", function(d){ return xScale(d.x)})
	.attr("cy", function(d){ return yScale(d.y)});
.axis path, line{
	stroke: gainsboro;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
like image 87
Gerardo Furtado Avatar answered Oct 05 '22 20:10

Gerardo Furtado


The easiest way is to use a lib like underscore.js to edit your data array.

From underscore docs:

flatten _.flatten(array, [shallow]) Flattens a nested array (the nesting can be to any depth). If you pass shallow, >the array will only be flattened a single level.

_.flatten([1, [2], [3, [[4]]]]);
-> [1, 2, 3, 4];

_.flatten([1, [2], [3, [[4]]]], true);
-> [1, 2, 3, [[4]]];

map _.map(list, iteratee, [context]) Alias: collect Produces a new array of values by mapping each value in list through a >transformation function (iteratee). The iteratee is passed three arguments: the >value, then the index (or key) of the iteration, and finally a reference to the >entire list.

_.map([1, 2, 3], function(num){ return num * 3; });
=> [3, 6, 9]
_.map({one: 1, two: 2, three: 3}, function(num, key){ return num * 3; });
=> [3, 6, 9]
_.map([[1, 2], [3, 4]], _.first);
=> [1, 3]

Underscore documentation

In your code you can do something like that:

var flatData = _.flatten(_.map(data, (d)=>d.values));
focus.selectAll(".dot")
    .data(data)
    .enter().append("circle")
    .attr("class", "dot")
    .attr("cx", function(d,i) { return x(d.date); })
    .attr("cy", function(d,i) { return y(d.count); })
    .attr("r", 4)
.style("fill", function(d,i) { return color(d.name); })
like image 26
rm4 Avatar answered Oct 05 '22 21:10

rm4