multi-line chart with circle points

9,307 views
Skip to first unread message

bryan

unread,
Jul 18, 2011, 5:20:45 PM7/18/11
to d3-js
Hello,

First, thanks for D3! I'm trying to figure out how to append
svg:circles at each point on a line chart. Getting multiple lines to
show up works fine using this:

var line = d3.svg.line()
.x(function(d) { return x(d.x) })
.y(function(d) { return -1 * y(d.y); })
;

var lines = this.graph_layer.selectAll(".data_line")
.data(data, function(d, i) { return i; })
.attr("d", function(d) { return line(d); })
;

lines.enter()
.append("svg:path")
.attr("d", function(d) { var l = line(d); return l; })
.attr("class", "data_line")
.attr("stroke", function(d, i) { return self.color(i); })
;


....but adding svg:circles to the points using something like this
(broken) snippet fails and only plots the first point:

var dots = this.graph_layer.selectAll(".data_line")
.data(data, function(d, i) { return i; })
;

dots.enter().append("svg:circle")
.attr("cx", function(d,i) { return x( d[i].x ); })
.attr("cy", function(d,i) { return -1 * y(d[i].y); })
.attr("r",7)
.on("mousedown", function(d,i){ console.log(d[i].x+" --
"+d[i].y);}
;

Thanks!

--Bryan

Mike Bostock

unread,
Jul 18, 2011, 5:30:41 PM7/18/11
to d3...@googlegroups.com
The cardinality of lines and circles are different. In the case of
lines, you map an array of data (e.g., [1, 2, 3, …]) to a single line.
With circles, you map a number (e.g., 1) to a single circle. So, if
you have multiple lines, then your data probably looks like a
two-dimensional array:

var data = [
[{x, y}, {x, y}, {x, y}, {x, y}, …],
[{x, y}, {x, y}, {x, y}, {x, y}, …],

];

If you want multiple circles, then you need instead a one-dimensional
array of {x, y} objects. You could use d3.merge(data) to flatten your
array:

https://github.com/mbostock/d3/wiki/Arrays#d3_merge

More likely, though, you'll want to create an svg:g element to group
the circles together for each line. Something like this:

var g = vis.selectAll("g.line")
.data(data);
.enter().append("svg:g")
.attr("class", "line");

g.append("svg:line")
.attr("d", line);

g.selectAll("circle")
.data(function(d) { return d; })
.enter().append("svg:circle")
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return -y(d.y); });

Mike

bryan

unread,
Jul 18, 2011, 7:43:41 PM7/18/11
to d3-js
Yes, the grouping approach worked perfectly. HUGE thanks!

Jeff Bordogna

unread,
Aug 9, 2011, 9:41:13 AM8/9/11
to d3...@googlegroups.com
Hey guys,
    I think this may be an easy problem, but I'm stuck, so thanks in advance for the help.  I'm trying to do a multi-line chart like Bryan, but I also want to add a hover effect which makes each circle bigger.  But, instead of just doing a mouseover on a particular line/circle like most examples I've seen, I want to do a mousemove event over the chart space, figure out the closest X value, and just highlight the circle at the X value for each line (a la the provotis example).


My data is stored as:

this.points = [
    [ {x: 0, y: 0}, {x: 5, y: 5}, etc.]
    [ {x: 10, y: 10}, {x:15, y:15}, etc.]
    ...
];

then created the lines and groups a la:

var g = me.vis.selectAll("g.line")
            .data(this.points)
            .enter().append("svg:g")
            .attr("class", "line");

        //if I use "svg:line", the element gets created but is not visible...not sure why..any ideas?
        g.append("svg:path")
            .attr("d", d3.svg.line()
            .x(function(d, i) {
                return x(d.x);
            }).y(function(d){
                return y(d.y);
            }))
            .style("stroke-width", 2)
            .style("fill", 'none')
            .style("stroke", '#000000');

        g.selectAll("circle")
            .data(function(d) { return d; })
            .enter().append("svg:circle")
            .attr("cx", function(d) { return x(d.x); })
            .attr("cy", function(d) { return y(d.y); })
            .attr("r", 1);

I added a rectangle over the grid to catch the mouseover:

me.vis.append("svg:rect")
            .attr("id","mouseRect")
            .attr("x", x(this.maxX))
            .attr("y", y(this.maxY))
            .attr("width", x(this.minX)-x(this.maxX))
            .attr("height",  y(this.minY)-y(this.maxY))
            .attr("fill", "blue")
            .attr("fill-opacity", 0)
            .on("mousemove", function(){me.vizMouseOver(me)})
            .on("mouseout", function(){me.vizMouseOut(me)});

and in the mousemove mouseover:

var currentLoc = me.x.invert(d3.svg.mouse(me.vis[0][0])[0]);  //why is it me.viz[0][0]?
var closestIndex = //code that has loop to find closest x value in data set

var circles = me.vis.selectAll("circle")
            .data(me.points, function(d){
                return d[closestIndex];
            })
            .attr("r", function(d) {
                return 8;
            });

Basically, I want the data function to only give me the circle at that point that I want highlighted.  What happens is that the data function returns a d that is each individual point, instead of the line's series, so I don't know what index it is.  Though, even when I use the "i" variable from the data function for this instead, the "r" function returns the whole array of items, instead of just the one:

var circles = me.vis.selectAll("circle")
            .data(me.points, function(d, i){
                if(i == closestIndex){
                    return d[i];
                }
            })
            .attr("r", function(d) {
                  //gives me the full series for the line that contains the circle
                   return 8;
            });

  I would appreciate any advice you had on what I'm missing to get this to work, and perhaps more importantly, guidance on a best practice for accomplishing this ( I doubt what I have is the best way).  I've tried a bunch of different variations of this and am all turned around now.  

Thanks!

Jeff Bordogna

unread,
Aug 9, 2011, 4:50:31 PM8/9/11
to d3...@googlegroups.com
any best practices for updating the points over multiple lines like this?  I understand it's a newbie question, so am happy to get just a bit of guidance and head off to do some more research/testing

Thanks!

Bob Monteverde

unread,
Aug 12, 2011, 11:26:14 AM8/12/11
to d3-js
Ok, this works great, but how do I get the dots to the same color as
the lines.. specifically, what's the best way to get the index of the
line you're on (the index provided to the dots functions I believe is
the index of the dot on the line, not the line)... here's my code:

var g = vis.selectAll('g.line')
.data(data)
.enter().append('svg:g')
.attr('class', 'line');

var lines = g.append('svg:path')
.attr('d', d3.svg.line()
.x(function(d) { return x(d.x) })
.y(function(d) { return y(d.y) })
)
.attr('fill', 'none')
.attr('stroke-width', 2)
.attr('stroke', function(d, i) {
return colors[i * 2 % 20];
});

var dots = g.selectAll('circle')
.data(function(d) { return d; })
.enter().append('svg:circle')
.attr('cx', function(d) { return x(d.x); })
.attr('cy', function(d) { return y(d.y); })
.attr('r', 2);


Also, I'm using this on an interactive chart where there's an update
function with this code to update the lines/dots, I feel like I'm
doing something wrong, though I am getting the correct results (but to
me, working does not imply "right"):

g.data(data);

lines
.data(data)
.attr('d', d3.svg.line()
.x(function(d) { return x(d.x) })
.y(function(d) { return y(d.y) })
);

dots
.data(function(d) { return d })
.attr('cy', function(d) { return y(d.y) });

Thanks in advance for the help.




On Jul 18, 5:30 pm, Mike Bostock <mbost...@cs.stanford.edu> wrote:
> The cardinality oflinesand circles are different. In the case oflines, you map an array of data (e.g., [1, 2, 3, …]) to a single line.
> With circles, you map a number (e.g., 1) to a single circle. So, if
> you have multiplelines, then your data probably looks like a

Jeff Bordogna

unread,
Aug 12, 2011, 12:09:28 PM8/12/11
to d3...@googlegroups.com
Hey Bob,
     I had the same question about the circle colors.  Though there may be a better way, but adding the colors to the group element worked for me:

var g = vis.selectAll('g.line')
.data(data)
.enter().append('svg:g')
.attr('class', 'line')
.style('fill', function(d,i){

     return colors[i * 2 % 20]; 
})
.style('stroke', function(d,i){

     return colors[i * 2 % 20];
});

var lines = g.append('svg:path')
.attr('d', d3.svg.line()
.x(function(d) { return x(d.x) })
.y(function(d) { return y(d.y) }) 
.attr('fill', 'none')
.attr('stroke-width', 2);


var dots = g.selectAll('circle')
.data(function(d) { return d; })
.enter().append('svg:circle')
.attr('cx', function(d) { return x(d.x); })
.attr('cy', function(d) { return y(d.y); })
.attr('r', 2);

Note that I still need "fill:none" in the lines variable if I only want it to be stroked...by setting the color at the group level, that sets it all the same, and then I can override in elements within that group if needed.

Hope this helps.

Benjamin West

unread,
Aug 12, 2011, 12:54:52 PM8/12/11
to d3...@googlegroups.com
Consider setting the same class on your circles and lines. Then you
can use a css rule to style it.

-bewest

Jeff Bordogna

unread,
Aug 15, 2011, 9:04:05 AM8/15/11
to d3...@googlegroups.com
I have this working, and I also have the updating of these lines working as well....however, in the latter case, the way I seem to have to set it up doesn't make sense to me so I hope someone can explain.

My data is set up as in your example, and I basically used your same code.  To update, I have:

 var g = this.viz.selectAll("g.line-group")
            .data(currentLines);

        this.viz.selectAll("path.line")
            .data(currentLines)
            .attr("d", d3.svg.line()
            .x(function(d, i) {
                return (x(i) + x.rangeBand()/2);
            }).y(function(d){
                return y(d.rate);
            }));

        g.selectAll("circle")
            .data(function(d, i) {
                return d;
            })
            .attr("cy", function(d) {
                return y(d.rate);
            })

If I remove the data method from the line drawing part, it doesn't work (though this is how it's set up in my initial draw), and If I change the this.viz.selectAll("path.line") to g.selectAll("line"), it also doesn't work.  Why can't I use the same methodology that I use to draw the lines (and that you use above), where I update the data in the group, and then redraw the attached lines, without having to specify a data arr.  I can see that the group data is getting updated.

Thanks for the help
Reply all
Reply to author
Forward
0 new messages