d3 / d3-shape

Graphical primitives for visualization, such as lines and areas.
https://d3js.org/d3-shape
ISC License
2.47k stars 307 forks source link

d3.curveMonotoneX may be unsuitable for stacked areas. #81

Open misha-panyushkin opened 7 years ago

misha-panyushkin commented 7 years ago

Creating stacked area chart with .curve(d3.curveLinear) produced correct interpolation: image

Creating the same chart with .curve(d3.curveMonotoneX) produced strange convexity for the highest area (between 3 and 2 points (X) from the end): image

Last 4 items from the series for the LOWEST area are: [0, 0] [0, 186] [0, 2294] [0, 2812.11] Last 4 items from the series for the HIGHEST area are: [0, 0] [186, 186] [2294, 2294] [2812.11, 21312.11]

mbostock commented 7 years ago

I’ve posted a reproduction that gives a better sense of what’s going on:

screen shot 2016-09-21 at 10 29 33 am

The problem is that the top line crosses over the bottom line.

My guess is that this isn’t fixable, in the sense that when you use a monotone curve on an area, the curves are computed independently and so there’s no way to guarantee that the top line doesn’t cross over the bottom line. And even if you fixed it for an area’s top and bottom line, you’d still have the problem that the top line of the lower series (in orange) would presumably be drawn above the bottom line of the higher series (in blue).

mbostock commented 7 years ago

Another way of thinking about this is that d3.curveMonotoneX preserves monotonicity in y (given monotonicity in x). So if you take the problematic series and shift it to the baseline (y0 = 0, i.e., not stacked), it works fine because the top line y1 never crosses y = 0:

screen shot 2016-09-21 at 10 34 58 am

But preserving y1y0 does not happen if you shift the area to make it stacked, because now you must preserve monotonicity in y1 - y0 (the difference between the top and bottom lines), not y.

I suppose what you might want here is a new curve type that preserves this other sort of monotonicity. It could use the same algorithm in d3.curveMonotoneX, except the input points would be shifted such that y0 = 0 when computing the tangents, and then after the resulting curves would be shifted back to the original y0 and y1. This probably isn’t too hard, but the downside is that there’d be no way to use such a curve type with d3.line to draw the top and bottom line independently.

Other solutions would be to use d3.curveBasis instead of d3.curveMonotoneX, which produces a smooth curve and is guaranteed to not cause crossover.

screen shot 2016-09-21 at 10 29 50 am

However, this has the effect of smoothing the data, which may be undesirable. You could also explicitly smooth the data yourself before constructing the area shape.

misha-panyushkin commented 7 years ago

The first solution will break the stacked structure of the data, and it wouldn't be over the lower series any more as I understood. That way I couldn't use it.

The second one will have undesirable behavior with smoothing as you mentioned. And the smoothing is not acceptable for my task.

By the way, if we look at another side of the areas: image

As we could see the upper series are interpolated well over the lower one for the left side. The upper series have zero width as for the right side, but behavior is different. Why?

mbostock commented 7 years ago

What are you referring to as the “first” solution? The new monotone curve type? That wouldn’t change your code much; you’d just replace d3.curveMonotoneX with d3.curveMonotoneDeltaX or some such. (The shifting approach I mentioned would happen inside the curve, when the tangents are computed.) I have no immediate plans to implement such a feature, but I’d review a pull request if someone wants to contribute it.

It works on the other side by coincidence: the point at which the top and bottom lines meet is an inflection point. That’s not true on the right side: when the lines meet, they are both sloping down.

If you want an immediate solution, your choices are to use a linear or basis curve, to implement a new curve type, or to smooth the input data before computing the curve.