Search code examples
javascriptd3.jsgridaxisarea

D3 graph with area fill not working


I would like get the area in the line graph to fill correctly. (Look at the picture to see problem)

Notice the area filled and the text under the fill:

enter image description here

Here is the code:

<!DOCTYPE html>
<style type="text/css">

.area {
    fill: lightsteelblue;
    stroke-width: 0;
}

</style>
<svg width="860" height="400"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>

var svg = d3.select("svg"),
    margin = {top: 20, right: 20, bottom: 30, left: 50},
    width = +svg.attr("width") - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom,
    g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var parseTime = d3.timeParse("%d-%b-%y");

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

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

var line = d3.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.close); });

d3.tsv("data.tsv", function(d) {
  d.date = parseTime(d.date);
  d.close = +d.close;
  return d;
}, function(error, data) {
  if (error) throw error;

  x.domain(d3.extent(data, function(d) { return d.date; }));
  y.domain([0, d3.max(data, function(d) { return d.close; })]);

var xAxis = d3.axisBottom(x)
    .ticks(d3.timeYear);

var yAxis = d3.axisRight(y)
    .tickSize(width);

var area = d3.area()
    .x0(function () {
    return 0,width;
})
    .x1(function (d) {
    return x(d.date);
}).y0(function () {
    return height,height;
})
    .y1(function (d) {
    return y(d.close);
});

var valueline = d3.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.close); });

g.append("g")
    .attr("transform", "translate(0," + height + ")")
    .call(customXAxis);

g.append("g")
    .call(customYAxis);

function customXAxis(g) {
  g.call(xAxis);
  g.select(".domain").remove();
  g.selectAll(".tick line")
  .attr("stroke", "#e9e9e9")
  .attr("height","510px")
  .attr("transform", "translate(0," + -height + ")");
}

function customYAxis(g) {
  g.call(yAxis);
  g.select(".domain").remove();
  g.selectAll(".tick:not(:first-of-type) line").attr("stroke", "#e9e9e9");
  g.selectAll(".tick text").attr("x", width - 18).attr("dy", -4);
}

  g.append("path")
      .datum(data)
      .attr("fill", "none")
      .attr("stroke", "steelblue")
      .attr("stroke-linejoin", "round")
      .attr("stroke-linecap", "round")
      .attr("stroke-width", 1.5)
      .attr("d", line);

g.append("path")
    .datum(data)
    .attr("class", "area")
    .attr("d", area);

});

</script>

Also if possible, if anyone knows, how can you get the area fill to not overlap the y-axis text and all the axis-lines, so the text and the axis-lines gets on top of the area fill.

And also one more thing, I've been trying for hours. How to get x-axis lines to create a grid together with the grey y-axis lines?

I would be grateful for any help. Thanks.


Solution

  • For your primary question:

    You are using a line generator to create an area graph - when filling the generated path, you simply connect the first and last points, hence your image. How does the generator "know" where the base of the filled area should be? A y axis value of zero may be common for the base, but it certainly isn't universal. In any event, the line generator would need to include points with a scaled y value of 0 to create the proper fill - this would not be an accurate line representing the data unless the start and end points had y values of 0, this is why d3 provides an area generator in addition to the line generator.

    Instead of d3.line, use d3.area (also, here's a canonical example likely using your data):

    var area = d3.area()
        .x(function(d) { return x(d.date); })
        .y1(function(d) { return y(d.close); })   // top of area
        .y0(yScale(0));                           // bottom of area
    

    the .y0 method is optional, if it isn't specified, it defaults to zero, but here's the documentation on it:

    If y is specified, sets the y0 accessor to the specified function or number and returns this area generator. If y is not specified, returns the current y0 accessor, which defaults to:

    function y() { return 0; }

    When an area is generated, the y0 accessor will be invoked for each defined element in the input data array, being passed the element d, the index i, and the array data as three arguments. See area.x0 for more information. (source).

    The y1 method represents the top of the graph (the same as the y accessor in your line graph), the y0 method represents the bottom, which in your case is constant.