Marc Rasmussen
Marc Rasmussen

Reputation: 20555

d3 re rendering chart exit is not a function

I am trying to make sure that i can update my charts data with D3 for this i have the following function:

fetchData(scope.filters.title_id, scope.filters.competence_id, scope.filters.from, scope.filters.to).then(function (data) {

    color.domain(d3.keys(data[0]).filter(function (key) {
        return key !== "timestamp";
    }));

    data.forEach(function (d) {
        d.timestamp = parseDate(d.timestamp);
    });

    var cities = color.domain().map(function (name) {
        return {
            name: name,
            values: data.map(function (d) {
                return {timestamp: d.timestamp, temperature: +d[name]};
            })
        };
    });

    x.domain(d3.extent(data, function (d) {
        return d.timestamp;
    }));

    y.domain([
        d3.min(cities, function (c) {
            return d3.min(c.values, function (v) {
                return v.temperature;
            });
        }),
        d3.max(cities, function (c) {
            return d3.max(c.values, function (v) {
                return v.temperature;
            });
        })
    ]);

    svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);

    svg.append("g")
        .attr("class", "y axis")
        .call(yAxis)
        .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 6)
        .attr("dy", ".71em")
        .style("text-anchor", "end")
        .text("Niveau");

    var city = svg.selectAll(".city")
        .data(cities)
        .enter().append("g")
        .attr("class", "city");

    city.append("path")
        .transition()
        .attr("class", "line")
        .attr("d", function (d) {
            return line(d.values);
        })
        .style("stroke", function (d) {
            return color(d.name);
        });

    city.append("text")
        .datum(function (d) {
            return {name: d.name, value: d.values[d.values.length - 1]};
        })
        .attr("transform", function (d) {
            return "translate(" + x(d.value.timestamp) + "," + y(d.value.temperature) + ")";
        })
        .attr("x", 3)
        .attr("dy", ".35em")
        .text(function (d) {
            return d.name;
        });

    city.exit().remove();

})

However whenever i run it i get city.exit is not a function

The chart does sort of re render just in an unexpected way:

Original chart

enter image description here

After re render

enter image description here

As you can see the chart data does not rerender but the old data is not removed.

What am i doing wrong?

Fiddle

fiddle

**The fiddle is constantly being updated with my progress and the answers i get!

Upvotes: 1

Views: 1192

Answers (2)

Cool Blue
Cool Blue

Reputation: 6476

You are adding new axes every time and appending new path and text elements every time.

Ok, here's a full solution...

  var margin = {
      top: 6,
      right: 80,
      bottom: 30,
      left: 30
    },
    width = 600 - 20 - margin.left - margin.right,
    height = 200 - margin.top - margin.bottom;

  var parseDate = d3.time.format("%d-%m-%Y").parse;

  var x = d3.time.scale()
    .range([0, width]);

  var y = d3.scale.linear()
    .range([height, 0]);

  var color = d3.scale.category10();

  var xAxis = d3.svg.axis()
    .scale(x)
    .tickSize(-height)
    .tickPadding(10)
    .tickSubdivide(true)
    .orient("bottom");

  var yAxis = d3.svg.axis()
      .scale(y)
      .tickPadding(10)
      .tickSize(-width)
      .tickSubdivide(true)
      .ticks(5)
      .orient("left")
      .tickFormat(d3.format(".0f"));

  var line = d3.svg.line()
    .interpolate("cardinal")
    .x(function (d) {
      return x(d.timestamp);
    })
    .y(function (d) {
      return y(d.temperature);
    });

  var svg = d3.select("#progressChart").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")

  svg.append("g")
    .attr("class", "y axis")
    .append("text")
    .attr("transform", "rotate(-90)")
    .attr("y", 6)
    .attr("dy", ".71em")
    .style("text-anchor", "end")
    .text("Niveau");

  var render = function (newData, t) {

    var data = fetchData(newData);

    color.domain(d3.keys(data[0]).filter(function (key) {
      return key !== "timestamp";
    }));

    data.forEach(function (d) {
      d.timestamp = parseDate(d.timestamp);
    });

    var cities = color.domain().map(function (name) {
      return {
        name: name,
        values: data.map(function (d) {
          return {
            timestamp: d.timestamp,
            temperature: +d[name]
          };
        })
      };
    });

    x.domain(d3.extent(data, function (d) {
      return d.timestamp;
    }));

    y.domain([
      d3.min(cities, function (c) {
        return d3.min(c.values, function (v) {
          return v.temperature;
        });
      }),
      d3.max(cities, function (c) {
        return d3.max(c.values, function (v) {
          return v.temperature;
        });
      })]);

    svg.selectAll(".x.axis")
      .call(xAxis);

    svg.selectAll(".y.axis")
      .transition().duration(t)
      .call(yAxis)

    var city = svg.selectAll(".city")
          .data(cities),
        cityEnter = city.enter().append("g")
          .attr("class", "city");

    cityEnter
      .append("path")
      .attr("class", "line");

    city.select(".line")
      .transition().duration(t)
      .attr("d", function (d) {
        return line(d.values);
      })
      .style("stroke", function (d) {
        return color(d.name);
      });

    cityEnter.append("text")
      .attr("x", 3)
      .attr("dy", ".35em");
    city.select("text")
      .text(function (d) {
        return d.name;
      })
      .transition().duration(t)
      .attr("transform", function (d) {
          var final = d.values[d.values.length - 1];
          return "translate(" + x(final.timestamp) + "," + y(final.temperature) + ")";
      });

    city.exit().remove();

  };

  var fetchData = function (newData) {
    if (!newData) {
      return [{
        Forventet: 8,
        Nuværende: 1,
        timestamp: "12-4-2015"
      }, {
        Forventet: 8,
        Nuværende: 2,
        timestamp: "12-5-2015"
      }, {
        Forventet: 8,
        Nuværende: 7,
        timestamp: "12-6-2015"
      }]
    } else {
      return [{
        Forventet: 2,
        Nuværende: 3,
        timestamp: "12-4-2015"
      }, {
        Forventet: 6,
        Nuværende: 5,
        timestamp: "12-5-2015"
      }, {
        Forventet: 4,
        Nuværende: 7,
        timestamp: "12-6-2015"
      }]
    }
  };

  render(false, 0);

  setTimeout(function () {
    render(true, 2000)
  }, 2000)
    .grid .tick {
      stroke: lightgrey;
      opacity: 0.7;
      shape-rendering: crispEdges;
    }
    .grid path {
      stroke-width: 0;
    }
    .axis path {
      fill: none;
      stroke: #bbb;
      shape-rendering: crispEdges;
    }
    .axis text {
      fill: #555;
    }
    .axis line {
      stroke: #e7e7e7;
      shape-rendering: crispEdges;
    }
    .axis, .axis-label {
      font-size: 12px;
    }
    .line {
      fill: none;
      stroke-width: 1.5px;
    }
    .dot {
      /* consider the stroke-with the mouse detect radius? */
      stroke: transparent;
      stroke-width: 10px;
      cursor: pointer;
    }
    .dot:hover {
      stroke: rgba(68, 127, 255, 0.3);
    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="progressChart"></div>

Upvotes: 3

altocumulus
altocumulus

Reputation: 21578

Your variable city contains the enter selection computed by the data join.

var city = svg.selectAll(".city")
    .data(cities)
    .enter()

This enter selection will, of course, have no function .exit() to return the elements to remove. The documentation of selection.enter() has a good example on how to combine the enter, update and exit selection:

var update_sel = svg.selectAll("circle").data(data)
update_sel.attr(/* operate on old elements only */)
update_sel.enter().append("circle").attr(/* operate on new elements only */)
update_sel.attr(/* operate on old and new elements */)
update_sel.exit().remove() /* complete the enter-update-exit pattern */

For your code the following should work:

var city = svg.selectAll(".city")
    .data(cities);

city.enter().append("g")
    .attr("class", "city");

Upvotes: 2

Related Questions