Mark O'Hare
Mark O'Hare

Reputation: 157

D3.js adding to a Multi-Linegraph path and updating axis

Im currently having a problem with updating my linegraph paths and axis in d3.js

What I would like to happen is, when new data is entered (preferably via a websocket, but at the minute through a simple button click) the line extends to the new point of the graph and, if needed, the axis adjusts to facilitate the new sales value.

The test data I'm using to try this is as follows:

Client : XYZ sales: 400 Time: 17:00

Which should be entered in their respective fields on the web form.

I've tried a few different methods but I'm still quite new to d3.js and I'm not sure if i'm selecting or binding data in the correct way, and my axis don't seem to update either.

Preferably, I would also like this code to work for all lines on the graph concurrently, so if the web form is paired with a database, it will update all the lines simultaneously at set intervals.

Any suggestions? It would be greatly appreciated.

jsfiddle of current build: https://jsfiddle.net/2bqzuue3/1/

Code for Button click event:

d3.select("button")
        .attr("id","submit")
        .on("click", function(d){
            var newClient = document.getElementById("client").value;
            var newSale = document.getElementById("sale").value;
            var newtime = document.getElementById("time").value;



            var update = {
                "Client" : newClient,
                "sale": newSale,
                "time": newtime
            };

            data.push(update);

            dataGroup = d3.nest()
                    .key(function(d){
                        return d.Client; //Decides which property (i.e key) to sort your data with
                                         //Useful for multiple lines on one graph
                    })
                    .entries(data);

            xScale = d3.time.scale()
                    .domain([d3.min(data, function(d){return parseTime(d.time);}),d3.max(data, function(d){return parseTime(d.time);})])

            yScale = d3.scale.linear()
                    .domain([d3.min(data, function(d){return d.sale;}) , d3.max(data, function(d){return d.sale;})]);

            vis.select(".x.axis")
                .transition()
                .duration(1000)
                .call(xAxis);

            vis.select(".y.axis")
                .transition()
                .duration(1000)
                .call(yAxis);

            lineGen = d3.svg.line()
                .x(function(d){
                    return xScale(parseTime(d.time));
                })
                .y(function(d){
                    return yScale(d.sale);
                })
                .interpolate("monotone");

            vis.selectAll("path")
                .data(dataGroup)
                .enter()
                .append("svg:path")
                .attr("d", lineGen(dataGroup.values))
                .transition()
                .duration(1000);




    });

*NOTE: you need to scroll down to see the text fields to enter test data in jsfiddle.

Upvotes: 0

Views: 59

Answers (1)

James Stewart
James Stewart

Reputation: 378

You have two problems in your code, which makes it hard to fix, as fixing either one independently will have no visible effect. You have to fix them both together.

First the easy one that is in your code above. The last section should look like this:

var paths = vis.selectAll("path")
    .data(dataGroup);
paths.enter()
    .append("svg:path");
paths.transition()
    .duration(1000)
    .attr("d", lineGen(dataGroup.values));

You were chaining everything off of the .enter() selection, which means that code would only run for new elements created by the data bind. By splitting this up, you ensure that the d attr is set for both existing and new elements. There's a pretty thorough explanation of this concept here.

Your second problem is a little more complicated, and is not in your code above, but in you jsfiddle. It happens in your dataGroup.forEach() loop. First of all, using a loop like this is actually skipping over using the d3 data bind, and you miss out on some benefits this way. you can see my jsfiddle below with a suggestion on how to write this instead. The actual problem you are having though is that you are creating 3 hidden paths to calculate length, and then 3 more to actually display. This means you now have 6 paths in the DOM, so when you do your data bind on a button click, the data binds to the 3 hidden paths, and not the ones that are displaying. You can get around using hidden paths to get the total path length like this:

path.attr("stroke-dasharray", function() {
    return this.getTotalLength() + " " + this.getTotalLength();
})
.attr("stroke-dashoffset", function() {
    return this.getTotalLength();
})

I made a couple other small changes to get everything working right, like fixing the scales in your button onClick function, creating a group for paths, and setting the stroke-dasharray on an update. See the details in the jsfiddle.

https://jsfiddle.net/9kkf6abw/

Going forward, I would recommend creating a new draw function that can handle both the initial draw as well as any updates. There's a lot of repeated code now that could be removed if you create that function, and with these changes you are almost there.

Upvotes: 1

Related Questions