Search code examples
javascriptd3.js

How can I properly fill the area between two line in D3.js?


I would like to fill in the area between red and green lines with a color (for example with blue color)

enter image description here

To do so, I modified the path elements to separate red and green data. However, now the color is a bit above the between lines as can be seen here:

enter image description here

Here is my script to create the plot and filling the area between curves:

<!DOCTYPE html>
<html>
<head>
  <title>Polar Chart with D3.js</title>
  <script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
  <script>
    let data = [];
    let features = [0, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]; // labels on the circle
    //generate the data
    for (var i = 0; i < 2; i++) {
      var point = {}
      //each feature will be a random number from 1-9
      features.forEach(f => point[f] = 1 + Math.random() * 8);
      data.push(point);
    }

    let width = 600;
    let height = 600;
    let svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height);

    //adding radial scale
    let radialScale = d3.scaleLinear()
      .domain([0, 10])
      .range([0, 250]);

    //adding ticks
    let ticks = [2, 4, 6, 8, 10];

    //adding circle
    svg.selectAll("circle")
      .data(ticks)
      .join(
        enter => enter.append("circle")
          .attr("cx", width / 2)
          .attr("cy", height / 2)
          .attr("fill", "none")
          .attr("stroke", "gray")
          .attr("r", d => radialScale(d))
      );

    //adding white background circle to remove filling in the center
    svg.append("circle")
      .attr("cx", width / 2)
      .attr("cy", height / 2)
      .attr("fill", "white") // fill the circle center area with white color
      .attr("stroke", "gray")
      .attr("r", radialScale(2));

    //adding text labels
    svg.selectAll(".ticklabel")
      .data(ticks)
      .join(
        enter => enter.append("text")
          .attr("class", "ticklabel")
          .attr("x", width / 2 + 5)
          .attr("y", d => height / 2 - radialScale(d))
          .text(d => d.toString())
      );

    //Plotting the Axes
    //map an angle and value into SVG
    function angleToCoordinate(angle, value) {
      let x = Math.cos(angle) * radialScale(value);
      let y = Math.sin(angle) * radialScale(value);
      return { "x": width / 2 + x, "y": height / 2 - y };
    }

    let featureData = features.map((f, i) => {
      let angle = (Math.PI / 2) + (2 * Math.PI * i / features.length);
      return {
        "name": f,
        "angle": angle,
        "line_coord": angleToCoordinate(angle, 10),
        "label_coord": angleToCoordinate(angle, 10.5)
      };
    });

    // draw axis line
    svg.selectAll("line")
      .data(featureData)
      .join(
        enter => enter.append("line")
          .attr("x1", width / 2)
          .attr("y1", height / 2)
          .attr("x2", d => d.line_coord.x)
          .attr("y2", d => d.line_coord.y)
          .attr("stroke", "black")
      );

    // draw axis label
    svg.selectAll(".axislabel")
      .data(featureData)
      .join(
        enter => enter.append("text")
          .attr("x", d => d.label_coord.x)
          .attr("y", d => d.label_coord.y)
          .text(d => d.name)
      );

    //draw shapes for actual data
    let line = d3.line()
      .x(d => d.x)
      .y(d => d.y);

    let area = d3.area()
      .x(d => d.x)
      .y0(d => d.y)
      .y1(height / 2) // The lower boundary for the area fill
      .curve(d3.curveCardinal); // You can use a different curve type if you prefer

    let colors = ["red", "green", "navy"];

    //helper function to iterate throughfields in each data point
    function getPathCoordinates(data_point) {
      let coordinates = [];
      for (var i = 0; i < features.length; i++) {
        let ft_name = features[i];
        let angle = (Math.PI / 2) + (2 * Math.PI * i / features.length);
        coordinates.push({
          x: width / 2 + Math.cos(angle) * radialScale(data_point[ft_name]),
          y: height / 2 - Math.sin(angle) * radialScale(data_point[ft_name])
        });
      }
      return coordinates;
    }

// Modify the path elements to separate red and green data
svg.selectAll(".path-red")
  .data([data[0]]) // Data for the red line
  .join(
    enter => enter.append("path")
      .datum(d => getPathCoordinates(d))
      .attr("class", "path-red")
      .attr("d", line)
      .attr("stroke-width", 3)
      .attr("stroke", "red")
      .attr("fill", "none") // No fill for the red line
  );

svg.selectAll(".path-green")
  .data([data[1]]) // Data for the green line
  .join(
    enter => enter.append("path")
      .datum(d => getPathCoordinates(d))
      .attr("class", "path-green")
      .attr("d", line)
      .attr("stroke-width", 3)
      .attr("stroke", "green")
      .attr("fill", "none") // No fill for the green line
  );

// Fill the area between the red and green lines
let areaFillData = [
  ...getPathCoordinates(data[0]),
  ...getPathCoordinates(data[1]).reverse()
];

svg.append("path")
  .datum(areaFillData)
  .attr("d", area)
  .attr("stroke-width", 0)
  .attr("fill", "blue") // Fill the area between the red and green lines with yellow color
  .attr("fill-opacity", 0.3); // You can adjust the opacity as you like


  </script>
</body>
</html>

I expect that only the area between curves/lines are colored with blue. In my implementation blue filling is crossing the line. How can I prevent it?


Solution

  • just replace

    .curve(d3.curveCardinal); 
    

    by

    .curve(d3.curveLinear);
    

    with d3.area, defining curve() allow you to tell d3 how you want it to interpolate between two point, curveLinear is just a straight path.