airbnb / visx

🐯 visx | visualization components
https://airbnb.io/visx
MIT License
19.42k stars 710 forks source link

[@visx/gradient, @visx/shape] Horizontal LinePath with LinearGradient doesn't render #1149

Open ashleypringle opened 3 years ago

ashleypringle commented 3 years ago

Hi there, we're switching over from using d3 to visx and it is very useful, but we ran into a peculiar issue. A LinePath that has a LinearGradient stroke never renders if it is a horizontal line, for example if every value in the data is 1. If there are any curves at all in the LinePath, then it renders with the gradient fine.

This is how our graph appears when given random values from 2 to -2: graph

And this is how it appears when given only values of 1: graphbroken

Our component code is fairly straightforward:

  const getX = (d: GraphData): Date => d.date;
  const getY = (d: GraphData): number => d.value;

  const xScale = scaleTime<number>({
    domain: extent(graphData, getX) as [Date, Date],
    range: [0, width]
  });

  const yScale = scaleLinear<number>({
    domain: [-2.1, 2.1],
    range: [height, 0]
  });

<svg width={width} height={height}>
{
  <Group key={`graphRender`}
    top={0}
    left={leftMargin}
  >
    <LinearGradient
      id="GradientRedGreen"
      from="red"
      to="green"
    />
    <LinePath<GraphData>
      curve={allCurves['curveMonotoneX']}
      data={graphData}
      x={d => xScale(getX(d)) ?? 0}
      y={d => yScale(getY(d)) ?? 0}
      stroke={`url(#GradientRedGreen)`}
      strokeWidth={4}
      strokeOpacity={1}
      shapeRendering="geometricPrecision"
    />
    {
      graphData.map((d, j) => {
        const cx = xScale(getX(d));
        const cy = yScale(getY(d));
        return <React.Fragment>
          <circle
            key={`${j}-gray`}
            r={4}
            cx={cx}
            cy={cy}
            stroke="gray"
            fill="gray"
          />
          <circle
            key={`${j}-green`}
            r={4.6}
            strokeWidth={1}
            cx={cx}
            cy={cy}
            stroke={circleColours[d.value]}
            fill="transparent"
          />
        </React.Fragment>
      })
    }
  </Group>
}
</svg>

Since the code is pretty simple we're pretty sure we're not doing something wrong, but it's not impossible that we're missing some detail. But the same graph rendered properly in d3, so it could also be a bug.

williaster commented 3 years ago

Hey @ashleypringle 👋 thanks for checking out visx. Sorry about this issue, I think I've actually hit it before and it took a long time to figure out what was up.

For me it turned out it wasn't a visx issue but an svg gotcha, check out this stack overflow question for more but the tl;dr is

You are getting caught out by the last paragraph in this part of the SVG specification

Keyword objectBoundingBox should not be used when the geometry of the applicable element has no width or no height, such as the case of a horizontal or vertical line, even when the line has actual thickness when viewed due to having a non-zero stroke width since stroke width is ignored for bounding box calculations. When the geometry of the applicable element has no width or height and objectBoundingBox is specified, then the given effect (e.g., a gradient or a filter) will be ignored.

objectBoundingBox is the default for gradientUnits so you need to use gradientUnits="userSpaceOnUse" [on the gradient] and then adjust the values to be appropriate for the different coordinate system.

We could potentially consider setting gradientUnits="userSpaceOnUse" as the default in @visx/gradient so others don't encounter this. Perhaps that's what d3 does but I'm not sure what your implementation looks like in d3 🤔

ashleypringle commented 3 years ago

Ah, thanks so much for the reply! It turns out gradientUnits="userSpaceOnUse" was indeed set in our d3 graph, and setting it in our visx graph fixed the problem. An easy detail to overlook, didn't realize it was so important. Thanks again!