loading...

How to create animated SVG using React

tornord profile image tornord Originally published at dev.to ・3 min read

When it comes to web-based infographics, SVG is by far the most popular format. SVG, Scalable Vector Graphics, is an xml markup language, like HTML. The svg elements are the basic graphical shapes, like <rect />, <circle/> and <path />, plus a group element <g/>. The elements can be transformed, e.g scaled, rotated and translated.

The svg format is popular because the svg-elements are fully interactive. They are like HTML elements, clickable and can be hovered. And also popular because it is easy to produce infographics. The underlying datasets (numbers, vectors and matrices) are mapped into arrays of e.g <rect />, <circle/> and <path />.

Here is a SVG bar chart example, consisting of five rect elements:

<svg version="1.1" viewBox="0 0 240 135">
  <style type="text/css">
    svg { background: #fbf4d5; }
  </style>
  <g id="root">
    <g>
      <rect x="60" y="55" width="24" height="60" fill="#236997"></rect>
      <rect x="84" y="31" width="24" height="84" fill="#52aaeb"></rect>
      <rect x="108" y="75" width="24" height="40" fill="#a75e07"></rect>
      <rect x="132" y="89" width="24" height="26" fill="#f4a22d"></rect>
      <rect x="156" y="68" width="24" height="47" fill="#f95b3a"></rect>
    </g>
  </g>
</svg>

It looks like this:

SVG Barchart

React, without any presentation here, is ideal for handling and producing the svg elements. Since React uses babel-script, the html/svg elements are mixed in the javascript code and the elements easily becomes components. E.g a <rect /> element is wrapped as a React functional component like this.

function Bar({ x, y, width, height, fill }) {
  return <rect x={x} y={y-height} width={width} height={height} fill={fill} />;
}

We call the component <Bar /> because we want to use it in the bar chart later. The rect element has origin at top/left, our Bar component has origin bottom/left.

Making basic React components and using them with the array function map is very useful. Mapping an array of numbers into an array of bars is done as a one-liner, where number is represented as then height of the rect element:

{[36,26, 9, 9,26].map((d, i) => <Bar x={60+24*i} y={115} width={24} height={d*95/36} fill={colors[i]} />)}

We scale the numbers and add some margins to make it look good in our 240x135 svg. The SVG is fully scalable. Our final result can have any size later.

When numbers changes it's important to animate the changes of the elements. Simple, because the user experience is augmented. With animations the changes becomes intuitive.

Here we use the React library react-move. It's a simple react tool to handle animations. The library provides the component <Animate />, that takes care of the transitions, frame by frame. You tell animate which variables to animate and with which timing function to do the transition. We pick the timing functions from the D3 library. D3 is a very useful library for data visualizations. The transitions becomes nudge like, using the timing function easeElasticOut.

To add animation to our bar chart example we use <Animate /> and wraps our <Bar /> component. We only want to animate height. It looks like this:

function AnimatedBar(props) {
  const { height } = props;
  return (
    <Animate
      start={{ height }}
      enter={{ height: [height], timing: tickTiming }}
      update={{ height: [height], timing: tickTiming }}
    >
      {(state) => <Bar {...props} height={state.height} />}
    </Animate>
  );
}

To create a running example we use ReactDOM render. We put our code in an <App /> component and add a empty svg element in the html dom.

ReactDOM.render(<App data={data} />, document.querySelector("#root"));

In this example we want the data to change every second. For this we need a setInterval call and we also need a state in the React component, to keep track of the current row index in our dataset. We put this together in a custom React hook called useTick:

function useTick(delay, initialIndex) {
  const [tick, setTick] = useState(initialIndex ? initialIndex : 0);
  useEffect(() => {
    const interval = setInterval(() => {
      if (!document.hidden) {
        setTick((tick) => tick + 1);
      }
    }, delay);
    return () => clearInterval(interval);
  }, []);
  return tick;
}

The complete <App /> component steps line by line in the dataset using useTick and the code looks like this:

var delay = 1000; // millisecond

function App({ data }) {
  var index = useTick(delay, 0);
  var values = data[index % data.length];
  return (
    <g>
      {values.map((d, i) => <AnimatedBar x={60+24*i} y={115} width={24} height={d*95/36} fill={colors[i]} />)}
    </g>
  );
}

The html and javascript are brought together in this pen:

Posted on by:

tornord profile

tornord

@tornord

I'm a fullstack developer coding in Node.js, Javascript and React. I like D3 when it comes to infographics.

Discussion

markdown guide