首页 > 解决方案 > 如何在d3中绘制两条不同长度(不同x值)的曲线之间的区域/填充

问题描述

问题

我正在尝试构建一个折线图,其中我在两条线之间着色(不同的颜色取决于哪条线在另一条线之上)。这在所有测试的情况下都非常适合线性弯曲。

但是,这需要与实际弯曲一起使用(例如curveBasis,如下所示)。如果线条具有完全相同的 x 值,则此方法非常有效;但是我们有以下情况:a) 一条线比另一条线长/短和/或 b) 一条线可能在另一条线没有丢失的中间缺少一个或多个 x 值。这是因为在两点之间绘制线的方式会根据非线性时之前/之后的点而变化。

一般来说,我理解为什么会发生这种情况;但我很难找到一个好的解决方案来让它真正按照我想要的方式工作。我很想至少指出正确的方向或给出一些想法(我考虑的一个想法列在底部)!

例子

这是它的工作原理curveLinear(看起来不错):

它如何与曲线线性(直线)一起使用

curveBasis如果两条线的 x 值相同(看起来不错),则如下所示:

它应该是什么样子

curveBasis如果两条线的 x 值不同(看起来不太好),它的实际外观如下:

目前看起来如何(不好)

当前代码/策略

这是当前的策略(请注意,我将线条称为好/坏,其中顶部的好线导致绿色填充,顶部的坏线导致红色填充)(删除了一些内容,如类名等以减少混乱) :

// I also set the domain and range appropriately for x/y--not shown here:
const x = d3.scaleTime();
const y = d3.scaleLinear();

// 1. Draw the lines "normally" (this is in a loop to build each line)
const lineData = d3.line()
  .defined(point => !isNaN(point.y))
  .x(point => x(point.x))
  .y(point => y(point.y))
  .curve(d3[lineCurve]);

d3Chart.append('path')
  .datum(points)
  .attr('d', lineData);

// 2. "Normalize" lines into the following format for each point (logic not shown here): {x, goodY, badY}
// Bind this data to a new svg group
const areaElement = d3Chart.append('g').datum(normlaizedData);

// 3. Clip paths and area paths

// Create the green fill clip path.
const goodLineClipPathId = `good-line-clip-path`;
areaElement.append('clipPath')
    .attr('id', goodLineClipPathId)
    .append('path')
    .attr('d', d3.area()
        .curve(lineCurve)
        .x(point => x(point.x))
        .y0(0)
        .y1(point => y(point.badY))
    );

// Create the red fill clip path.
const badLineClipPathId = `bad-line-clip-path`;
areaElement.append('clipPath')
    .attr('id', badLineClipPathId)
    .append('path')
    .attr('d', d3.area()
        .curve(lineCurve)
        .x(point => x(point.x))
        .y0(height)
        .y1(point => y(point.badY))
    );

// Create the red fill.
areaElement.append('path')
    .attr('clip-path', `url(#${badLineClipPathId})`)
    .attr('d', d3.area()
        .curve(lineCurve)
        .x(point => x(point.x))
        .y0(point => y(point.goodY))
        .y1(point => y(point.badY))
    );

// Create the green fill.
areaElement.append('path')
    .attr('clip-path', `url(#${goodLineClipPathId})`)
    .attr('d', d3.area()
        .curve(lineCurve)
        .x(point => x(point.x))
        .y0(point => y(point.badY))
        .y1(point => y(point.goodY))
    );

深思熟虑的解决方案

我的一个想法是“克隆”确切的 svg 线,但切断开始/结束(使线的其余部分保持不变)并将这些线用作区域的顶部/底部(并用直线垂直关闭末端行);但是路径数据本身使用了弯曲,所以改变开始/结束仍然会影响线(除非有办法解决这个问题)。

这是我想到的另一个想法:不要“规范化”线条并创建额外的剪辑来“切断”末端(在屏幕截图上绘制的垂直黑线处);但即使我这样做了,仍然会有问题(如箭头所示)。

可能的解决方案 - 仍然有问题

标签: d3.jssvg

解决方案


这不是一个很好的解决方案,但我发布是因为它是一个解决方案(或至少是部分解决方案)。

在没有深入细节的情况下,我注意到问题实际上有两个可能的原因(一个或两个都可能导致间隙或出血):

  1. 这些行不会以相同的 x 值开始和/或结束。
  2. 这些行不共享 x 值与实际 y 值的 1 对 1 映射(换句话说,两行中至少有一个是“缺失”另一行中没有缺失的相应 x 值的 ay 值) .

“解决方案”

注意:我只用curveBasis--other curving types 对此进行了测试,可能不会以同样的方式工作。

  1. “填充”缺失的 y 值(修复了上面的原因 #2)——我通过在最近的左/右非缺失点之间进行线性插值来做到这一点(如果缺失值位于一行的末尾或开头,则最近的非- 缺失的点值被重新使用)。在标准化行和原始行中都执行此操作,但不要在原始行中的点和不在标准化行中的点之间进行插值 - 在这种情况下只需重新使用(参见下面的示例)。
  2. “一式三份”标准化的开始/结束点(修复了上面的原因#1)——我通过在标准化数据开始和结束行数据的 x 值处将同一点再克隆两次来做到这一点。只需要在您的原始(非规范化)行中执行此操作 - 规范化行已经开始/结束于它们需要的位置。这适用于curveBasis,因为曲线考虑了哪些点 - 这个解决方案基本上强制一种线“结束”,即使它在一条线的中间(下面的文档引用源):

[curveBasis] 使用指定的控制点生成三次基样条。第一个和最后一个点是三重的,这样样条线从第一个点开始,到最后一个点结束......

例子

第 1 行(x 值):

[3, 4, 5, 7, 8, 9, 10, 11]原来的

[3, 4, 5, 6 (null y), 7, 8, 9, 10, 11]标准化,预操作

[3, 4, 5, 6 (interpolated y), 7, 8, 9, 10, 11]归一化,后操作

[3, 3, 3, 4, 5, 6 (interpolated y), 7, 8, 9, 10, 11, 11, 11]原创,后期处理

请注意,对于第 1 行,我们可以跳过将起点/终点重复三次,因为它们已经是原始行的起点/终点

第 2 行(x 值):

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13]原来的

[3, 4, 5, 6, 7, 8, 9, 10, 11 (null y)]标准化,预操作

[3, 4, 5, 6, 7, 8, 9, 10, 11 (re-used y from 10)]归一化,后操作

[1, 2, 3, 3, 3, 4, 5, 6, 7, 8, 9, 10, 11 (re-used y from 10), 11 (re-used y from 10), 11 (re-used y from 10), 13]原始的,后期操作(注意我们不会在 10 和 13 之间进行插值以获得 11,因为 13 不存在于标准化行中

它是什么样子的?

不是那么好 - 但是,嘿,没有间隙或出血!箭头指向我们“三重”点的位置,以迫使曲线“结束”在那里。

在此处输入图像描述

我们要使用这个吗?

不见得。我仍在寻找更好的解决方案;但这是我迄今为止想出的。


推荐阅读