Realtime charting on React Native using data from ESP32

Code walkthrough for realtime charting components of React Native, based on data streams coming from Arduino sensors. The featured star of this post is react-native-svg.

June 12, 2020

What is covered

Code repository

Primers

Installing react-native-svg

After we add the package we still need to do some manual edits both for Android and for iOS for certain versions. Take a look at the official documentation.

Charts Screen

/src/screens/ChartsScreen.js is getting the data from the 3 Arduino sensors as the sensorData prop from its parent component. At line 12 we assign a value to the slotsPerWidth constant. Its purpose is to tell the ChartsScreen component how many times it should add data on top of previous data before clearing the screen and start anew. So, in our case, we only have 100 slots to spread along the width of the device screen.

To manage the incoming data stream we employ the static method getDerivedStateFromProps. First of all, we ensure we are doing nothing and we are only resetting the value assigned to chartData key of the state, if this screen is not focused, meaning it is not the current screen. The variable counter keeps track where are we inside of that slotsPerWidth limit.

If those 3 arrays with sensor data are empty (lines 14-23) we seed them with the first batch of data. If we are somewhere in the middle of the slotsPerWidth interval, we simply concatenate existing arrays with the last batch of sensorData (lines 23-32). If we reached the right margin of the screen (the slotsPerWidth limit) we need to reset chartData from the state. We create a sense of visual continuity by keeping the last value (lines 32-48).

static getDerivedStateFromProps(props, state) {
    const { chartData } = state;
    const {
      sensorData,
      navigation: { isFocused },
    } = props;

    if (!isFocused()) {
      counter = 0;
      return { chartData: { ...initialState } };
    }

    if (sensorData.length && sensorData.length === 3) {
      if (!chartData.flow.length) {
        counter++;
        return {
          chartData: {
            flow: [sensorData[0]],
            volume: [sensorData[1]],
            pressure: [sensorData[2]],
          },
        };
      } else if (counter < slotsPerWidth) {
        counter++;
        return {
          chartData: {
            flow: [...chartData.flow, sensorData[0]],
            volume: [...chartData.volume, sensorData[1]],
            pressure: [...chartData.pressure, sensorData[2]],
          },
        };
      } else {
        counter = 2;
        return {
          chartData: {
            flow: [chartData.flow[chartData.flow.length - 1], sensorData[0]],
            volume: [
              chartData.volume[chartData.volume.length - 1],
              sensorData[1],
            ],
            pressure: [
              chartData.pressure[chartData.pressure.length - 1],
              sensorData[2],
            ],
          },
        };
      }
    } else {
      return null;
    }
  }

The render method of /src/screens/ChartsScreen.js is only returning those 3 Chart components if the screen is focused.

Chart component

The props we have at our disposal to customize the Chart component (/src/components/Chart.js) are as follows:

<Chart
  key="flow"
  data={flow}
  maxValue={1900}
  minValue={1750}
  slotsPerWidth={slotsPerWidth}
  width={width}
  height={height}
  marginBottom={20}
  lineColor="rgba(95, 92, 1, 1)"
  lineThickness={2}
  chartBackground="#17204d"
  horizontalGridLinesCount={5}
  gridColor="rgba(65, 95, 93, .4)"
  gridThickness={1}
  unit="ml"
  axisTooClose={10}
  labelsColor="rgba(255, 255, 255, 0.8)"
  labelsFontSize={12}
  marginLeft={60}
  labelsMarginLeft={15}
/>

The essence of the /src/components/Chart.js component is that it is using a rectangle Rect (the background) and a Polyline SVG React components having the Grid component on top of them. The meat is, of course, the convertArrayToPolylinePoints function. Its role is to compute the ratio of each value over the absolute difference between maximum and minimum values. Then it is using that ratio on the height of the chart to find the Y coordinate of that point. Please remember that in the case of SVG components zero Y coordinate is at the top of the scene. The X coordinate it is given by i cursor of the array times the fixed slotWidth.

const convertArrayToPolylinePoints = ({
  data,
  height,
  slotWidth,
  maxValue,
  minValue,
  marginLeft,
}) => {
  let polylinePoints = '';

  if (!data.length) {
    return polylinePoints;
  }

  for (let i = 0; i < data.length; i++) {
    const Y = Math.ceil(
      height -
        (height * Math.abs(data[i] - minValue)) / Math.abs(maxValue - minValue),
    );

    polylinePoints += `${slotWidth * i + marginLeft},${Y}`;

    if (i < data.length - 1) {
      polylinePoints += ' ';
    }
  }

  return polylinePoints;
};

Grid component

The props for the Grid component (/src/components/Grid.js) can be set like so:

<Grid
  gridCount={horizontalGridLinesCount}
  marginLeft={marginLeft}
  maxValue={maxValue}
  minValue={minValue}
  width={width}
  height={height}
  marginBottom={marginBottom}
  gridColor={gridColor}
  gridThickness={gridThickness}
  unit={unit}
  axisTooClose={axisTooClose}
  labelsColor={labelsColor}
  labelsFontSize={labelsFontSize}
  labelsMarginLeft={labelsMarginLeft}
/>

What we are rendering is multiple SVG Line and Text components as the grid lines and grid labels. The following lines of code give us the most valuable figures netHeight, spaceBetweenHorizontalGridLines, deltaValuesBetweenHorizontalGridLines, zeroY because now we know where to start drawing the lines and where to put the grid labels. We are gradually computing the x1, x2, y1, y2 of each Line component and we are pushing that into the gridLines array.

const netHeight = height - marginBottom;

const spaceBetweenHorizontalGridLines = Math.floor(
  netHeight / (gridCount + 1),
);

const deltaValuesBetweenHorizontalGridLines = Math.floor(
  Math.abs(maxValue - minValue) / (gridCount + 1),
);

const gridLines = [];
const gridLabels = [];

// add 0 axis
const zeroY =
  netHeight -
  (netHeight * Math.abs(minValue)) / Math.abs(maxValue - minValue);

We only render a grid line if it's not too close to the 0 axis (lines 72-75 of /src/components/Grid.js). At the end we render the content of gridLines and gridLabels arrays as children of a Svg component.