svgd3.jschartsdata-visualizationstacked-area-chart

How to add non linear curve between any two points of area chart in d3.js


I recently started with d3.js.I am working on a stacked area chart in d3 which looks similar to the below chart, stacked area chart

const stack = d3.stack().keys(["aData", "bData"]);
const stackedValues = stack(data);
const stackedData = [];

stackedValues.forEach((layer, index) => {
  const currentStack = [];
  layer.forEach((d, i) => {
  currentStack.push({
   values: d,
   year: data[i].year
  });
});
 stackedData.push(currentStack);
 });


      const yScale = d3
.scaleLinear()
.range([height, 0])
.domain([0, d3.max(stackedValues[stackedValues.length - 1], dp => dp[1])]);
const xScale = d3
       .scaleLinear()
       .range([0, width])
       .domain(d3.extent(data, dataPoint => dataPoint.year));

 const area = d3
             .area()
            .x(dataPoint => xScale(dataPoint.year))
            .y0(dataPoint => yScale(dataPoint.values[0]))
            .y1(dataPoint => yScale(dataPoint.values[1]));

    const series = grp
       .selectAll(".series")
       .data(stackedData)
       .enter()
      .append("g")
      .attr("class", "series");

   series
    .append("path")
     .attr("transform", `translate(${margin.left},0)`)
    .style("fill", (d, i) => color[i])
    .attr("stroke", "steelblue")
   .attr("stroke-linejoin", "round")
   .attr("stroke-linecap", "round")
  .attr("stroke-width", strokeWidth)
 .attr("d", d => area(d));

I have a requirement to be able to add non linear curve between any two points. I have made a very basic outline chart just to explain my point. outline chart

I tried using the curve function but it changes the whole line to the provided curve (here is the example code https://codepen.io/saif_shaik/pen/VwmqxMR), I just need to add a non linear curve between two points. is there any way to achieve this?


Solution

  • You could create a custom curve generator. This could take a number of different forms. I'll recycle a previous example by tweaking one of the existing d3 curves and using its point method to create a custom curve.

    Normally a custom curve applies the same curve between all points, to allow different types of lines to connect points, I'll keep track of the current point's index in the snippet below.

    The custom curve in the snippet below is returned by a parent function that takes an index value. This index value indicates which data point should use a different curve between it and the next data point. The two types of curves are hand crafted - some types curves will present more challenges than others.

    This produces a result such as:

    enter image description here

    function generator(i,context) {
      var index = -1;
      return function(context) {
        var custom = d3.curveLinear(context);
        custom._context = context;
        custom.point = function(x,y) {
          x = +x, y = +y;
          index++;
          switch (this._point) {
            case 0: this._point = 1; 
              this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y);
              this.x0 = x; this.y0 = y;        
              break;
            case 1: this._point = 2;
            default: 
              // curvy mountains between values if index isn't specified:
              if(index != i+1) {
                var x1 = this.x0 * 0.5 + x * 0.5;
                var y1 = this.y0 * 0.5 + y * 0.5;
                var m = 1/(y1 - y)/(x1 - x);
                var r = -100; // offset of mid point.
                var k = r / Math.sqrt(1 + (m*m) );
                if (m == Infinity) {
                  y1 += r;
                }
                else {
                  y1 += k;
                  x1 += m*k;
                }     
                this._context.quadraticCurveTo(x1,y1,x,y); 
                // always update x and y values for next segment:
                this.x0 = x; this.y0 = y;        
                break;
              }
              // straight lines if index matches:
              else {
                // the simplest line possible:
                this._context.lineTo(x,y);
                this.x0 = x; this.y0 = y;  
                break;         
              }
          }
        }
        return custom;
      }
    }
    
    
    var svg = d3.select("body")
      .append("svg")
      .attr("width", 500)
      .attr("height", 300);
      
      
    var data = d3.range(10).map(function(d) {
      var x = d*40+40;
      var y = Math.random() * 200 + 50;
      
      return { x:x, y:y }
      
    })
    
    
    var line = d3.line()
      .curve(generator(3))  // striaght line between index 3 and 4.
      .x(d=>d.x)
      .y(d=>d.y)
      
      
    svg.append("path")
      .datum(data)
      .attr("d",line)
      .style("fill","none")
      .style("stroke-width",3)
      .style("stroke","#aaa")
      
    svg.selectAll("circle")
      .data(data)
      .enter()
      .append("circle")
      .attr("cx",d=>d.x)
      .attr("cy",d=>d.y)
      .attr("r", 2)
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

    This line will work with canvas as well if you specify a context as the second argument of generator(). There are all sorts of refinements that could be made here - the basic principle should be fairly adaptable however.