Problem
I am trying to build a line chart where I shade between two lines (different colors based on which line is above the other). This works perfectly for linear curving in all cases tested.
However, this needs to work with actual curving (such as curveBasis, as shown below). This works perfectly if the lines have the exact same x values; but we have cases in which a) one line is longer/shorter than another and/or b) one line may be missing one or more x values in the middle of the line that the other line isn't missing. This is because how a line is drawn between two points changes based on what points come before/after when it's non-linear.
Generally speaking, I understand why this is happening; but I'm having a hard time finding a good solution to make this actually work the way I'd like. I'd love to at least be pointed in the right direction or given some ideas (one idea I considered is listed at the bottom)!
Examples
Here's how it works with curveLinear (looks good):
Here's how it looks with curveBasis if the x values are the same for both lines (looks good):
Here's how it actually looks with curveBasis if the x values are not the same for both lines (doesn't look good):
Current Code/Strategy
Here's the current strategy (note that I refer to the lines as good/bad, where good line on top results in green fill and bad line on top results in red fill) (some stuff removed, like class names, etc to reduce clutter):
// 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))
);
Considered Solutions
One idea I had was to "clone" the exact svg lines but cut off the beginning/end (leaving the remainder of the line the same) and use those lines as the top/bottom of the areas (and close the ends with straight vertical lines); but the path data itself makes use of curving, so changing the start/end would still affect the line (unless there's a way around this).
Here's another idea I thought of: don't "normalize" the lines and create additional clipping to "cut off" the ends (at the vertical black lines drawn on the screenshot); but even if I did that, there would still be issues (as indicated by the arrows).





