2

I have written a reusable d3 line chart (code below). Unfortunately it only updates properly when the data array passed to it is updated; if it is passed a new data array, it does not update at all - you can see it in this jsfiddle.

Here is the html, which is mostly the embedded demo calling script:

<style>
  path { stroke: purple; stroke-width: 2px; fill: none; }
</style>
<body>
<div id="demo"></div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="demoChart.js"></script>
<script>
  var chart = demoChart();
  var pts = [[[0,0],[200,0.25],[500,1]]];

  d3.select("#demo").datum(pts).call(chart);
  setTimeout(function() {
    console.log("Modifying data array");
    pts[0][2][1] = 0.5;
    d3.select("#demo").datum(pts).call(chart);
  },1000);

  setTimeout(function() {
    console.log("Passing new data array");
    d3.select("#demo").datum([[[0,1],[200,0.45],[500,0]]]).call(chart);
  },2000);
</script>

You can see the second time it calls chart it directly updates a single point in the data array (pts[0][3][1] = 0.5), and the chart animates properly. The third time it passes a new data array, and the chart does not change.

Here is the demoChart.js code (based on the reusable charts pattern):

function demoChart() {

    function xs(d) { return xScale(d[0]) }
    function ys(d) { return yScale(d[1]) }

    var xScale = d3.scale.linear().domain([0, 500]).range([0, 400]),
        yScale = d3.scale.linear().domain([0, 1]).range([400, 0]),
        line = d3.svg.line().x(xs).y(ys);

    function chart(selection) {
        selection.each(function(data) {
            console.log("passed data: ", data);

            // Select the svg element, if it exists; otherwise create it
            var svg = d3.select(this).selectAll("svg").data([1]);
            var svgGEnter = svg.enter().append("svg").append("g");

            // Select/create/remove plots for each y, with the data
            var plots = svg.select("g").selectAll(".plot").data(data);
            plots.exit().remove();
            var plotsEnter = plots.enter().append("g").attr("class","plot");
            plotsEnter.append("path");

            // Update the line paths
            plots.selectAll("path")
                .transition()
                    .attr("d", function(d,i) {
                        console.log("transitioning line with data: ", d);
                        return line.apply(this, arguments);
                    });

            svg.attr("width", 400).attr("height", 400);

        });
    }

    return chart;
}

I suspect I am missing something fundamental about how d3 works.

How can I make the chart update properly when a new data array is passed?

Racing Tadpole
  • 4,270
  • 6
  • 37
  • 56

2 Answers2

6

Where you update the line paths, via

plots.selectAll("path")

it needs to be

plots.select("path")

see mbostock's explanation of the subtle but crucial difference.

Here's a working fiddle, which also has a second path added, to verify that it works across plots.

Community
  • 1
  • 1
meetamit
  • 24,727
  • 9
  • 57
  • 68
  • That's got it! Thank you, and thanks for the working example and the link to mbostock's explanation. I haven't quite got the hang of how to choose between `select` and `selectAll` yet, but I'll keep an eye on it from now on... – Racing Tadpole Jan 21 '15 at 23:24
0

I actually had the same issue today. So I may be able to help out. I noticed you're calling setTimeout to update the data in your html.

It seems you are calling chart() inside of that setTimeout. The only problem with that is that you aren't resetting your ranges.

Instead you should try calling demoChart(), or adding new ranges inside of your chart(). Your ranges are:

function xs(d) { return xScale(d[0]) }
function ys(d) { return yScale(d[1]) }

var xScale = d3.scale.linear().domain([0, 500]).range([0, 400]),
    yScale = d3.scale.linear().domain([0, 1]).range([400, 0]),
    line = d3.svg.line().x(xs).y(ys);

If that doesn't fix it, it may because you are updating with two setTimeouts initiating each second simultaneously. So the second data is overwriting the first immediately here:

setTimeout(function() {
  console.log("Modifying data array");
  pts[0][2][1] = 0.5;
  d3.select("#demo").datum(pts).call(chart);
},1000);

setTimeout(function() {
  console.log("Passing new data array");
  d3.select("#demo").datum([[[0,1],[200,0.45],[500,0]]]).call(chart);
},1000);

Here's an article I found helpful: http://www.d3noob.org/2013/02/update-d3js-data-dynamically-button.html

Tyler Davis
  • 873
  • 7
  • 15
  • Thanks Tyler. Yes, the second timeout with a delay of 1000 was bad, it should have been 2000 (I've edited it now) - the result is the same either way though. Also, I should have said, I just used setTimeout for the example here, in my real application it's in response to an event. meetamit's answer seems to have solved the problem. – Racing Tadpole Jan 21 '15 at 23:27