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
- react-native-svg "provides SVG support to React Native on iOS and Android, and a compatibility layer for the web."
- Bluetooth Low Energy Client on a React Native Application
- Bluetooth Low Energy Server on ESP32 development board
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.