1

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?

Sksaif Uddin
  • 642
  • 1
  • 15
  • 22
  • Do check my answer below. You were not clear if you want to have a curved line BETWEEN your point or you want to ADD additional curved lines. For the later you can add additional "line generator" like for arc that would take two data points and add a curve between them. So the call would be attr("d" => line(d)) and the data any (sub)point array. – Matt Sergej Rinc Mar 17 '21 at 11:57
  • @MattSergejRinc i am sorry i was not clear , i need a curved line BETWEEN two points. – Sksaif Uddin Mar 17 '21 at 13:10
  • OK, be specific please. So, between two specific points (leaving other area straight edge lines already drawn)? Please provide an example, like curved line between 2002 and 2003 for aData and between 2004 and 2006 - maybe you need curved line(s) between non-adjacent points etc). Your sketched chart doesn't look very similar to the D3.js generated one so it's hard to tell what you really need. – Matt Sergej Rinc Mar 18 '21 at 01:00

3 Answers3

2

I simplied your path by removing precision in: https://yqnn.github.io/svg-path-editor/

You can use that editor to play with d-path, and learn where/how you want to change that d-path String.

Copy the d-path below and paste it in: https://yqnn.github.io/svg-path-editor/

<svg height="300" width="600"><g transform="translate(30,0)">
<g transform="translate(-28.5,-90)">
<g class="series">
<path stroke="steelblue" stroke-linejoin="round" 
      stroke-linecap="round" stroke-width="1.5" 
      d="M0 257 15 250C30 242 61 227 91 216C122 205 152 197 182 199C213 200 243 211 274 208
         C304 205 334 188 365 169C395 151 425 129 456 116C486 102 517 96 532 93
         L547 90 547 280 532 280C517 280 486 280 456 280C425 280 395 280 365 280
         C334 280 304 280 273 280C243 280 213 280 182 280C152 280 122 280 91 280
         C61 280 30 280 15 280L0 280Z" 
      style="fill: lightgreen;">
</path></g></g></g></svg>
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
1

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.

Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
0

Just change the curve type. The curveBasis approximates a curve between points but it doesn't cross them. So use either the usually used "curveCardinal" type or maybe even "curveCatmullRom" that are curves passing the data points.

// Fake data
const data = [
  {
    year: 2000,
    aData: 50,
    bData: 300
  },
  {
    year: 2001,
    aData: 150,
    bData: 50
  },
  {
    year: 2002,
    aData: 200,
    bData: 100
  },
  {
    year: 2003,
    aData: 130,
    bData: 50
  },
  {
    year: 2004,
    aData: 240,
    bData: 80
  },
  {
    year: 2005,
    aData: 380,
    bData: 10
  },
  {
    year: 2006,
    aData: 420,
    bData: 200
  }
];
const color = ["lightgreen", "lightblue"];
// Create SVG and padding for the chart
const svg = d3
  .select("#chart")
  .append("svg")
  .attr("height", 300)
  .attr("width", 600);

const strokeWidth = 1.5;
const margin = { top: 0, bottom: 20, left: 30, right: 20 };
const chart = svg.append("g").attr("transform", `translate(${margin.left},0)`);

const width = +svg.attr("width") - margin.left - margin.right - strokeWidth * 2;
const height = +svg.attr("height") - margin.top - margin.bottom;
const grp = chart
  .append("g")
  .attr("transform", `translate(-${margin.left - strokeWidth},-${margin.top})`);

// Create stack
const stack = d3.stack().keys(["aData", "bData"]);
const stackedValues = stack(data);
const stackedData = [];
// Copy the stack offsets back into the data.
stackedValues.forEach((layer, index) => {
  const currentStack = [];
  layer.forEach((d, i) => {
    currentStack.push({
      values: d,
      year: data[i].year
    });
  });
  stackedData.push(currentStack);
});

// Create scales
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]))
//.curve(d3.curveBasis)
.curve(d3.curveCardinal)
//.curve(d3.curveCatmullRom.alpha(0.5))
;

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));

const dotsGreen = chart
  .selectAll(".gdot")
  .data(data)
  .enter()
  .append("circle")
  .attr("class", "gdot")
  .attr("cx", function(d) { 
    return xScale(d.year)
  })
  .attr("cy", d => yScale(d.aData))
  .attr("r", 4)
  .attr("fill", "green");

const dotsBlue = chart
  .selectAll(".bdot")
  .data(data)
  .enter()
  .append("circle")
  .attr("class", "bdot")
  .attr("cx", function(d) { 
    return xScale(d.year)
  })
  .attr("cy", d => yScale(d.aData+d.bData))
  .attr("r", 4)
  .attr("fill", "blue");

// Add the X Axis
chart
  .append("g")
  .attr("transform", `translate(0,${height})`)
  .call(d3.axisBottom(xScale).ticks(data.length));

// Add the Y Axis
chart
  .append("g")
  .attr("transform", `translate(0, 0)`)
  .call(d3.axisLeft(yScale));
#chart {
  text-align: center;
  margin-top: 40px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="chart"></div>

The working codePen forked from your code is also here:

changed CodePen pen

Note: I've added the code for two circle groups (not SVG G element) that you can simply remove. They just serve for a proof where curves are drawn near data points based on curve type scripted.

Your sketched chart looks like you want to have a curve only between two given points. For that you would have to change the curve call to use the running index (e.g. (d,i)) in a function that would return different curve type based on chosen index (or indeces).

ADDED: you can play with different D3.js curve types here:

D3 curve explorer

Matt Sergej Rinc
  • 565
  • 3
  • 11