d3.js 如何在面积图的任意两点之间添加非线性曲线

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

我最近开始 d3.js.I 我在 d3 中制作堆积面积图,它看起来类似于下图,

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

我需要能够在任意两点之间添加非线性曲线。我做了一个非常基本的大纲图表来解释我的观点。

我尝试使用 curve 函数,但它会将整条线更改为提供的曲线(这里是示例代码 https://codepen.io/saif_shaik/pen/VwmqxMR),我只需要添加一个两点之间的非线性曲线。有什么办法可以实现吗?

我通过删除精度简化了你的路径:https://yqnn.github.io/svg-path-editor/

您可以使用该编辑器来玩转 d-path,并学习 where/how 您想要更改的 d-path String.

复制下面的 d 路径并将其粘贴到: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>

只需更改曲线类型即可。 curveBasis 近似于点之间的曲线,但不与它们交叉。因此,可以使用通常使用的“curveCardinal”类型,甚至可以使用“curveCatmullRom”,它们是通过数据点的曲线。

// 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>

从您的代码派生出来的工作代码笔也在这里:

changed CodePen pen

注意:我已经为两个圆组(不是 SVG G 元素)添加了代码,您可以简单地删除它们。它们仅用于根据脚本化的曲线类型在数据点附近绘制曲线的证明。

您的草图看起来像您想要在两个给定点之间 一条曲线。为此,您必须更改曲线调用以在函数中使用 运行 索引(例如 (d,i)),该函数将 return 基于所选索引(或索引)的不同曲线类型。

添加:您可以在此处使用不同的 D3.js 曲线类型:

D3 curve explorer

您可以创建自定义曲线生成器。这可以采取多种不同的形式。我将通过调整一条现有的 d3 曲线并使用其点方法创建自定义曲线来回收之前的

通常情况下,自定义曲线会在所有点之间应用相同的曲线,以允许不同类型的线连接点,我将在下面的代码片段中跟踪当前点的索引。

下面代码片段中的自定义曲线由采用索引值的父函数返回。此索引值指示哪个数据点应在它和下一个数据点之间使用不同的曲线。这两种类型的曲线是手工制作的 - 有些类型的曲线会比其他类型的曲线面临更多挑战。

这会产生如下结果:

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>

如果您将上下文指定为 generator() 的第二个参数,则此行也适用于 canvas。这里可以进行各种改进 - 然而,基本原则应该具有相当的适应性。