Christian Kuroki

FULL STACK DEVELOPER, DATABASES GURU IN BUENOS AIRES, ARGENTINA

Composing SVG with React

This is a step by step tutorial to implement an <svg> based component, that display a Bar Chart.

If you want to go directly to the code you can get a complete example at github, or check this demo

  1. Development environment

If you are not using it yet, I will recommend you to try Create React App.

Install create-react-app and create our new project

$ npm install -g create-react-app
$ create-react-app mychart
$ cd mychart
$ npm start

After executing last command you will receive some output like this:

Compiled successfully!

The app is running at:

  http://localhost:3000/

Now you can visit that link and this page will display:

React app screenshot

  1. Create component

With you preferred code editor, create a file called BarChart.js under src directory, and add this code.

import React, { PropTypes } from 'react';

const BarChart = (props) => {
  return (
    <svg width='400' height='300'>
      <text x='50' y='150'> Hello SVG </text>
      <path d='M 0 0 H 400 V 300 H 0 V0' 
            fill='transparent' stroke='black'/>
    </svg>
  );
};

export default BarChart;

We are creating a Functional Stateless component here, is functional because is defined as an ES6 Arrow function , and stateless because does not maintain is own state, only render based on properties received as parameters (props).

Note that we are drawing an SVG of 400 pixels wide and 300 height, containing:

  • A <text> element on x=50 and y=150 position.
  • A <path> element. This contains this commands:
M 0 0   Move cursor to x=0 y=0  
H 400   Draw an horizontal line until x=400 
V 300   Draw an vertical line until y=300
H 0   Draw an horizontal line until x=0 
H 0   Draw an vertical line until y=0 

If you keep account of coordinates this make a box around our SVG.

  1. Using our new component

Now is time to display our component. Edit src/App.js file to look like this :

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import BarChart from './BarChart';

class App extends Component {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <br/>
        <BarChart />                  
      </div>
    );
  }
}

export default App;

After updating this you will to see something like this on your browser:

React app screenshot 2

  1. Adding parameters

Now we will add properties to make our component dynamic.

  • List of properties:

width, height : Size of our graphic. (Numbers)

dataSeries : Values to be displayed on Chart. (Array of Numbers)

dataLabels : Labels, one for each Value. (Array of Strings)

First we will add this properties validation block

BarChart.propTypes = {
  width: PropTypes.number.isRequired,
  height: PropTypes.number.isRequired,
  dataSeries: PropTypes.array.isRequired,
  dataLabels: PropTypes.array
};

Note that we define width, height and dataSeries as required. Now we can update our previous code including new properties.

import React, { PropTypes } from 'react';

const BarChart = (props) => {
  let {width,height,dataSeries,dataLabels} = props;
  let box=(
      <path d={`M 0 0 H ${width} V ${height} H 0 V0`} 
            fill='transparent' stroke='black'/>
          );
  // Check data to draw
  let noData= (dataSeries.length === 0 );
  let graph=[];

  if (noData){
    graph=(<text x={width/8} y={height/2}>No data...</text>);
  }  

  return (
    <svg width={width} height={height}>
      {box}
      {graph}
    </svg>
  );
};

BarChart.propTypes = {
    width: PropTypes.number.isRequired,
    height: PropTypes.number.isRequired,
    dataSeries: PropTypes.array.isRequired,
    dataLabels: PropTypes.array
};

export default BarChart;

Some code comments:

ES6 Destructuring assignment, we get local vars from props object fields.

let {width,height,dataSeries,dataLabels} = props;

ES6 String interpolation expression, build a string injecting variables values

d={`M 0 0 H ${width} V ${height} H 0 V0`}

Changed our Hello SVG message to No data … to be displayed only if dataSeries prop have no elements.

  1. Add parameters to main page

Now we can add out properties to our object container (App.js)

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import BarChart from './BarChart';    

class App extends Component {
  render() {
  let dataSeries=[100,110,120,80];
  let dataLabels=['North','East','South','West'];
  let width=400;
  let height=300;
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <br/>
        <BarChart width={width} height={height} 
                  dataSeries={dataSeries}
                  dataLabels={dataLabels} />                  
      </div>
    );
  }
}

export default App;
  1. Lets draw the bars
import React, { PropTypes } from 'react';

const BarChart = (props) => {
  let {width,height,dataSeries,dataLabels} = props;
  let box=(
      <path d={`M 0 0 H ${width} V ${height} H 0 V0`} 
            fill='transparent' stroke='black'/>
          );
  // Check data to draw
  let noData= (dataSeries.length === 0 );
  let graph=[];

  if (noData){
    graph=(<text x={width/8} y={height/2}>No data...</text>);
  } else {
    let barWidth=width/dataSeries.length;
    let bars=dataSeries.map( (elem,index) => {
      let barx = index*barWidth;
      return (<rect key={`bar_${index}`} 
                    x={barx} y={height-elem} 
                    width={barWidth} height={elem}
                    style={{fill: 'cyan',stroke: 'gray'}}/>);
      });
    graph=bars;
  }

  return (
    <svg width={width} height={height}>
      {box}
      {graph}
    </svg>
  );
};

BarChart.propTypes = {
    width: PropTypes.number.isRequired,
    height: PropTypes.number.isRequired,
    dataSeries: PropTypes.array.isRequired,
    dataLabels: PropTypes.array
};

export default BarChart;

After last changes, page output will be:

React app screenshot 3

Code comments:

This code

let bars=dataSeries.map( (elem,index) => {
let barx = index*barWidth; // x position of each bar
return (<rect key={`bar_${index}`} x={barx} y={height-elem} 
                    width={barWidth} height={elem}
                    style={{fill: 'cyan',stroke: 'gray'}}/>);
});

will produce a <rect> element like this :

<rect x="0" y="200" width="100" height="100" 
      style="fill: cyan; stroke: gray;"/>

for each value on dataSeries, using Array map() method.

  1. Improving output

Add labels for each bar

let labels=dataLabels.map( (elem,index) => { 
  let barx=index*barWidth;
  return (<text key={`lbl_${index}`} 
                x={barx} y={height + (xAxisHeight/2)} 
                style={{fontSize: '.9em'}}>
                {elem}
          </text>);
});

Show value on top of each bar

let values=dataSeries.map( (elem,index) => { 
  let barx=index*barWidth;
  return (<text key={`row_${index}`} 
                x={barx+(barWidth/3)} 
                y={height-elem} 
                style={{fontSize: '.9em'}}>
            {elem}
          </text>);
});

Compose bars,labels and values into graph component, using ES6 destructuring.

  graph=[...bars,...labels,...values];

In svg component, add room for labels (xAxisHeight variable)

<svg width={width} height={height+xAxisHeight}>
  {box}
  {graph}
</svg>

Final version of component is:

import React, { PropTypes } from 'react';

const BarChart = (props) => {
  let {width,height,dataSeries,dataLabels} = props;
  let box=(
      <path d={`M 0 0 H ${width} V ${height} H 0 V0`} 
            fill='transparent' stroke='black'/>
          );
  // Check data to draw
  let noData= (dataSeries.length === 0 );
  let graph=[];
  const xAxisHeight = 50;

  if (noData){
    graph=(<text x={width/8} y={height/2}>No data...</text>);
  } else {
    // Draw Bars
    let barWidth=width/dataSeries.length;
    let bars=dataSeries.map( (elem,index) => {
      let barx = index*barWidth;
      return (<rect key={`bar_${index}`} 
                    x={barx} 
                    y={height-elem} 
                    width={barWidth} height={elem}
                    style={{fill: 'cyan',stroke: 'gray'}}/>);
      });

    // Return a <text> element, containing labels for each val
    let labels=dataLabels.map( (elem,index) => { 
      let barx=index*barWidth;
        return (<text key={`lbl_${index}`} 
                      x={barx} 
                      y={height + (xAxisHeight/2)} 
                      style={{fontSize: '.9em'}}>
                  {elem}
                </text>);
    });

    // Return a <text> element, with actual value for each bar
    let values=dataSeries.map( (elem,index) => { 
      let barx=index*barWidth;
        return (<text key={`row_${index}`} 
                      x={barx+(barWidth/3)} 
                      y={height-elem} 
                      style={{fontSize: '.9em'}}>
                  {elem}
                </text>);
    });

    // Compose bars,labels and values
    graph=[...bars,...labels,...values];
  }

  return (
    <svg width={width} height={height+xAxisHeight}>
      {box}
      {graph}
    </svg>
  );
};

BarChart.propTypes = {
    width: PropTypes.number.isRequired,
    height: PropTypes.number.isRequired,
    dataSeries: PropTypes.array.isRequired,
    dataLabels: PropTypes.array
};

export default BarChart;

Now our app will look like this: React app screenshot 4

  1. Where to go now
  • Change, add or remove values from dataSeries and dataLabels arrays and see how it changes the output.

  • Try this demo, that is basically same code plus some components allowing to change parameters dinamically.

  • Get complete code from here, that is basically same code plus some components allowing to change parameters dinamically.

Posted April 4, 2017