4

I want to draw multiple svg lines (using D3.js) from a nested object structure like this:

data = [ 
{ Name: "jim", color: "blue", Points: [{x:0, y:5 }, {x:25, y:7 }, {x:50, y:13}] },
{ Name: "bob", color: "green", Points: [{x:0, y:10}, {x:25, y:30}, {x:50, y:60}] }
];

var line_gen = d3.svg.line().interpolate("linear")
                .x(function(d){return d.x;})
                .y(function(d){return d.y;}); 

My first attempt, based on this question was to use svg:g to represent the first nesting level, then access the second nesting level within the group to render the lines:

var g = svg.selectAll(".linegroup")
.data(data).enter()
.append("g")
.attr("class", "linegroup")
.style("stroke", function(d) { return d.color; })
.style("fill", "none");

g.selectAll(".line")
.data(function(d) { return d.Points;})
.enter().append("svg:path")
  .attr("d", line_gen);

But this did not render any lines. Instead, inside each group I got an empty path tag for each of the three data points. So apparently the shape of the data is causing problems.

<g class="linegroup" style="stroke: #0000ff; fill: none;">
    <path></path>
    <path></path>
    <path></path>
</g>

I did find one solution (will post as answer) that doesn't use groups, but I would still like to know why the group solution doesn't work.

Community
  • 1
  • 1
explunit
  • 18,967
  • 6
  • 69
  • 94

2 Answers2

2

Here is an answer, largely based on this question except that it doesn't use d3.entries:

data = [ 
{ Name: "jim", color: "blue", Points: [{x:0, y:5 }, {x:25, y:7 }, {x:50, y:13}] },
{ Name: "bob", color: "green", Points: [{x:0, y:10}, {x:25, y:30}, {x:50, y:60}] }
];

svg = d3.select("body").append("svg:svg").attr("width", 100).attr("height", 100)

var line_gen = d3.svg.line().interpolate("linear")
                .x(function(d){return d.x;})
                .y(function(d){return d.y;});

svg.selectAll(".line")
  .data(data)
  .enter().append("svg:path")
    .attr("d", function(d) { return line_gen(d.Points); })   // <<<
    .style("stroke", function(d) { return d.color; })
    .style("fill", "none");

The key approach here is to not return the line_gen function directly to the "d" attribute but to wrap it in another function that passes the correct nested array from the outer object.

See also this great discussion on d3.entries vs. d3.values. d3.entries seems to be suggested a lot for similar problems but I don't think it really helps in my case.

Community
  • 1
  • 1
explunit
  • 18,967
  • 6
  • 69
  • 94
1

The reason is that you want to draw a line for each element in your list -- nested selections are meant for cases where more levels exist. For example, if you had a list with lists as elements and each list element contained a set of paths (e.g. to group sets of curves), nested selections would be appropriate.

What's happening in your case is that d3 is generating a new path for each element of Points -- that is, one for each point.

Lars Kotthoff
  • 107,425
  • 16
  • 204
  • 204
  • oh I see. So it was appropriate in the linked question because they actually *wanted* a circle tag for each data point, whereas I want a single path tag for all data points on the line. It seems like an example of a [leaky abstraction](http://en.wikipedia.org/wiki/Leaky_abstraction) where you have to know that SVG uses path mini-language as attribute rather than distinct tags to represent the points on a line. – explunit Jan 28 '13 at 16:57
  • Well, not entirely -- the question would be which data elements to treat separately. If you want to put points on a line, they are arguably not treated separately. I do agree that it can be confusing however. – Lars Kotthoff Jan 28 '13 at 17:00