Friday, November 3, 2017

Recharts bar chart with multiple colors

I needed once to add a bar chart using Recharts  library but with multiple colors, with two coloros rotated in the bar. Something like this:



There is no such functionality out of the box, but it's doable. Recharts provide a possibility to customize bar shape. Bar component has an attribute shape that accepts a function or a component, to render the shape of the bar.

Since Recharts uses SVG under the hood the first thing I did I found how people get such shapes using pure SVG. To achive that we need to define 2 things: a pattern and a mask based on that pattern.

<svg width="120" height="120" viewBox="0 0 120 120"
    xmlns="http://www.w3.org/2000/svg">

  <!-- Pattern -->
  <pattern id="pattern-stripe" 
        width="8" height="8" 
         patternUnits="userSpaceOnUse"
         patternTransform="rotate(45)">
    <rect width="4" height="8" transform="translate(0,0)" fill="white"></rect>
  </pattern>
  <!-- Mask -->
  <mask id="mask-stripe">
   <rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-stripe)" />
  </mask>
  
  <!-- Reactangle that uses mask -->
  <rect x="10" y="10" width="100" height="100" mask='url(#mask-stripe)' />
</svg>

I put the mask and the pattern in the SVG tag and also a rect tag that uses the mask to define how this react will be filled. If you save it as SVG file and open in a browser you will see something like this:


It's pure SVG and it has nothing to do with react and recharts. However we already know that we are on the right track.
Let's create a function that will render a colored rectangle:

const CandyBar = (props) => {
 const {    
    x: oX,
    y: oY,
    width: oWidth,
    height: oHeight,
    value,
    fill
  } = props;
  
  let x = oX;
  let y = oHeight < 0 ? oY + oHeight : oY;
 let width = oWidth;
  let height = Math.abs(oHeight);

 return (
   <rect fill={fill}
          mask='url(#mask-stripe)'
          x={x}
          y={y}
          width={width}
          height={height} />
    );
};

No we can use it as shape parameter for the Bar component. There is nothing interesting here, we just so cover here the case when height is negative, it happens when we need to display a negative value.  We also set here the mask, to define how the react will be filled. This is how looks a bar with custom shape:

 <Bar dataKey="a" fill="green" shape={<CandyBar />} />

Now let's put everything together:

const data = [
      {name: 'Page A', a: 400, b: 240, c: 240},
      {name: 'Page B', a: 300, b: 139, c: 221},
      {name: 'Page C', a: -200, b: -980, c: 229},
      {name: 'Page D', a: 278, b: 390, c: 200},
      {name: 'Page E', a: 189, b: 480, c: 218},
      {name: 'Page F', a: 239, b: -380, c: 250},
      {name: 'Page G', a: 349, b: 430, c: 210}
];

const CandyBar = (props) => {
  const {    
    x: oX,
    y: oY,
    width: oWidth,
    height: oHeight,
    value,
    fill
  } = props;
  
  let x = oX;
  let y = oHeight < 0 ? oY + oHeight : oY;
  let width = oWidth;
  let height = Math.abs(oHeight);

  return (
   <rect fill={fill}
       mask='url(#mask-stripe)'
          x={x}
          y={y}
          width={width}
          height={height} />
    );
};



const CustomShapeBarChart = React.createClass({
  render () {
    return (
    <div>    
      <BarChart width={700} height={300} data={data}
            margin={{top: 20, right: 30, left: 20, bottom: 5}}>
        
        <pattern id="pattern-stripe" 
         width="8" height="8" 
         patternUnits="userSpaceOnUse"
         patternTransform="rotate(45)">
         <rect width="4" height="8" transform="translate(0,0)" fill="white"></rect>
        </pattern>
        <mask id="mask-stripe">
        <rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-stripe)" />
        </mask>

         <XAxis dataKey="name"/>
         <YAxis/>
         <CartesianGrid strokeDasharray="3 3"/>
         <Bar dataKey="a" fill="green" isAnimationActive={false} shape={<CandyBar />} />
         <Bar dataKey="b" isAnimationActive={false} fill="red" />
         <Bar dataKey="c" isAnimationActive={false} shape={<CandyBar fill="#8884d8" />} />
      </BarChart>
    </div>
    );
  }
})

ReactDOM.render(
  <CustomShapeBarChart />,
  document.getElementById('container')
);

It will render fallowing chart:



Here is a working fiddle

There are several important things here:
1. BarChart component generates SVG tag.
2. Mask and Pattern tags should be inside SVG tag. That is why we put them as children of the BarChart
3. Bar component uses CandyBar to define how the shape will be rendered.
4. CandyBar generates a rect with the proper mask that is located by Id.