In the previous article we finished the more logic part, we are ready to transition to the visual(chart, table etc).
the main purpose being to play with the real-time data visually, with a card showing real-time subscribers, a live updating chart showing subscribers, unsubs and tier changers, and lastly integrate a table, to of-course show tabular data.
Browsers are highly optimized and capable, we can do all sort of stuff: streaming, Machine learning, data processing there's no facet the JS world cannot touch, the how well is debatable.
Again like the previous article, React experience is assumed, "should-know" concepts will not be explained but implied.
Subscribers card.
create a new Stats.tsx component in the realtime folder.
// Stats.tsx
const Statscard = ({content, heading, icon}: {content: number, heading: string, icon: string})=> {
return (
)
}
export default Statscard
I am not going to separate the css from the components(not the best practice), in favor of explanation speed and a shorter article.
The jsx:
return (
<div style={{width: "50%", height: "auto", padding: "1em 1.4em", margin: "1em 1.4em", boxShadow: "0 11px 15px -7px rgb(0 0 0 / 20%), 0 24px 38px 3px rgb(0 0 0 / 14%), 0 9px 46px 8px rgb(0 0 0 / 12%)"}}>
<section style={{width: "100%", textAlign: "center"}}>
{content}
</section>
<div style={{display: "flex", alignItems: "center", justifyContent:"center", gap: "4px", width: "100%"}}>
<div>{icon}</div>
<h2>{heading}</h2>
</div>
</div>
)
Import the card to the main(index.tsx) realtime component, we will only show subscribers count, you can however show other stats.
...
import Statscard from "./Stats"
const RealTime:React.FC = ()=> {
const [point, setPoint] = useState<typeof queu[0]>()
return (
...
)
}
The idea behind point as a state, if you recall we get our data in a queue, and point is going to hold a single(think of it a time series data set) data point dequeued from that queue and
everytime queue changes which is 1000ms, we update point, with the code below:
useEffect(()=> {
if(queu?.length !== 0){
const m = queu.shift() // getting a point(the object with subs, unsubs, stats etc)
setPoint(m)
}
}, [queu])
Now we can consume the data, each point at time.
return (
<div style={{display: "flex", width: "100vw", height: "100vh", justifyContent: "flex-start", gap: "5px"}}>
<div style={{flex: 1}}>
<Statscard content={point?.subscriptions.length ?? 0} heading={"subscribers"} icon={"š¤"}/>
{/*Chart here */}
</div>
<div style={{flex: 1, display: "flex", height: "100%", alignItems: "center", justifyContent: "center"}}>
{/*Table here */}
</div>
</div>
)
Spin up the live server, the card should be showing subscribers as they are trickling in.
And note because of hot reloading, you might need to restart the server sometimes, if you see any issues.
The stats card is complete, and we can make things even better with a chart, who doesn't love charts?
Chart.js
A tool or a library is what you make of it, Chart.js is simple enough for beginners to get a chart in the browser quickly, yet dynamic enough for advanced developers to break the browser with it, it's a thing of beauty, don't be fooled by the simple initial look, if it's your first encounter.
Install chart.js and supporting types:
npm i chart.js
npm i -D @types/chart.js
chart.js is not React(not aware of any hooks or V DOM), we have to encapsulate it around the react virtual dom events(state updates etc), which we usually handle directly in chart.js, luckly our chart is simple.
In realtime create a component 'Series.tsx', which will hold our time series chart:
import { useEffect, useRef, useState } from "react"
import Chart, { ChartTypeRegistry } from "chart.js/auto"
// data expected by the Timeseries component
type stats = {
time: number;
subscriptions: number;
unsubs: number;
down_or_upgrade: number;
}[]
const Timeseries = ({stats}: {stats: stats})=> {
const [chart, setChart] = useState<Chart<keyof ChartTypeRegistry, any[], any>>()
// reference to the canvas element
const canvasEl = useRef(null)
useEffect(()=> {
}, [stats])
return (
<div>
<canvas style={{width: "100%"}} ref={canvasEl}></canvas>
</div>
)
}
export default Timeseries
Chart.js needs a canvas to paint a chart to, given the use of Refs
to get a reference to it,
The useEffect does all the heavy lifting, from checking the validity of the data to creating a chart:
useEffect(()=> {
if(stats && stats.length !== 0){
if(stats.length > 5){
stats = stats.slice(stats.length-6)
}
if(!canvasEl.current)
return
const l = Chart.getChart(canvasEl.current)
if(l){
l.destroy()
}
const config = {
type: 'line',
options: {
animation: false,
plugins: {
legend: {
display: true
},
tooltip: {
enabled: true
}
}
},
data: {
labels: stats.map(row => {const n = new Date(row.time); return n.getHours() +":" + n.getSeconds()}),
datasets: [{data: stats.map(all => all.subscriptions), label: "subscriptions", fill: false}, {data: stats.map(all => all.unsubs), label: "unsubs", fill: false}, {data: stats.map(all => all.down_or_upgrade), label: "down_or_updgrade", fill: false}]
}
}
try {
// @ts-ignore
const c = new Chart(canvasEl.current, {type: "line", ...config});
setChart(c)
} catch (error) {
}
}
}, [stats])
defensive programming is good programming, we don't want to even attempt to draw the chart if we don't have any or valid data
if(stats && stats.length !== 0){
}
we want to show 5 data points at a time, remember the data is moving in time, so eventually all the data will be displayed in the chart, hence the cap of 5 per "frame", that's good UX, we don't want to overwhelm the consumer:
if(stats.length > 5){
stats = stats.slice(stats.length-6)
}
Chart.js is not React, we have to allow react to get the reference to the canvas first before we do anything with chart.js, we are at the mercy of React, if the reference is still null we can't paint.
if(!canvasEl.current)
return
We also cannot draw on a canvas which Chart.js is already using(dirty), that's an error, we need to destroy the previous chart if there's any so we can paint a new one.
// Chart.getChart will return undefined if the canvas is clean
const l = Chart.getChart(canvasEl.current)
if(l){
l.destroy()
}
The following is basic configuration, you can do more, from formatting to animations, for our purpose it'll work, you can refer to the docs for more options, the important option being type, in our case we want to plot a line chart.
const config = {
type: 'line',
options: {
animation: false,
plugins: {
legend: {
display: true
},
tooltip: {
enabled: true
}
}
},
data: {
labels: stats.map(row => {const n = new Date(row.time); return n.getHours() +":" + n.getSeconds()}),
datasets: [{data: stats.map(all => all.subscriptions), label: "subscriptions", fill: false}, {data: stats.map(all => all.unsubs), label: "unsubs", fill: false}, {data: stats.map(all => all.down_or_upgrade), label: "down_or_updgrade", fill: false}]
}
}
the datasets options takes an array of data objects, Chart.js will handle plotting them in the same axis(same chart), with labels being x axes and datasets y.
data: {
labels: stats.map(row => {const n = new Date(row.time); return n.getHours() +":" + n.getSeconds()}),
datasets: [{data: stats.map(all => all.subscriptions), label: "subscriptions", fill: false}, {data: stats.map(all => all.unsubs), label: "unsubs", fill: false}, {data: stats.map(all => all.down_or_upgrade), label: "down_or_updgrade", fill: false}]
}
if there's no error we can now plot the chart
try {
// @ts-ignore
const c = new Chart(canvasEl.current, {type: "line", ...config});
setChart(c)
} catch (error) {
}
everytime stats changes the useEffect will run again, plotting the new data, which gives us the real-time look or update, which is not that efficient, Chart.js does provide a method to patch or update data directly into a chart, for our purpose recreating the chart will suffice.
import the timeseries component to Realtime\index.tsx
, and render the component under the statscard.
import Timeseries from "./Series"
...
<div style={{flex: 1}}>
<Statscard content={point?.subscriptions.length ?? 0} heading={"subscribers"} icon={"š¤"}/>
{ stats && stats[0] !== undefined ? <Timeseries stats={stats}/> : <></>}
</div>
if you peek at your browser, you should see a live updating chart, and onwards to the table.
Mui Table
In the repo you will find both the MUI and react-table implementations, MUI is simpler and has a basic design, so I chose to explain it.
Install MUI:
npm i @emotion/react @emotion/styled @fontsource/roboto @mui/material
open app.css
and paste the following:
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
create MUITable.tsx
inside the components folder, and do all the necessary imports:
import React, {useEffect, useState} from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
This is the basic implemention directly from the MUI documentation, the
Fisher yates algorithm, is used to show random data, we will cap the table also at 5 records, hence the shuffle at each re-render.
function randomize (arr)
{
// Start from the last element and swap
// one by one. We don't need to run for
// the first element that's why i > 0
for (let i = arr.length - 1; i > 0; i--)
{
// Pick a random index from 0 to i inclusive
let j = Math.floor(Math.random() * (i + 1));
// Swap arr[i] with the element
// at random index
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
When construction the MUI table, we do the same thing we did with stats, capping the data at 5 record, the table will show only unsubscribers.
export default function MuiTable({data}: {data: Array<Record<any,any>>}) {
const [rows, setRows] = useState([])
useEffect(()=> {
// shuffle array and show different data at each update
if(data.length > 5){
randomize(data)
setRows(data.slice(6))
}else{
setRows(data)
}
}, [data])
return (
)
}
This is probably the most self explanatory code we have written so far, that's why I like MUI tables, the ff constructs the actual visual table:
return (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 600 }} aria-label="Realtime Data">
<TableHead>
<TableRow>
<TableCell>firstName</TableCell>
<TableCell align="right">email </TableCell>
<TableCell align="right">sex </TableCell>
<TableCell align="right">subscriptionTier </TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow
key={row._id}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell component="th" scope="row">{row.firstName}</TableCell>
<TableCell align="right">{row.email}</TableCell>
<TableCell align="right">{row.sex}</TableCell>
<TableCell align="right">{row.subscriptionTier}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
Finally import the table and extract unsubscribes data:
// realtime\index.tsx
import MuiTable from "../MUITable"
const [data_, setData] = useState([])
const [point, setPoint] = useState<typeof queu[0]>()
useEffect(()=> {
if(queu?.length !== 0){
const m = queu.shift()
// unsubs
if(m)
setData(m.unsubs)
setPoint(m)
}
}, [queu])
<div style={{flex: 1, display: "flex", height: "100%", alignItems: "center", justifyContent: "center"}}>
{/*{ data_.length !== 0 && cols && <Table data={data_} columns={cols}/>} */}
<MuiTable data={data_}/>
</div>
With a few lines of code, we got an updating real-time table, the application of-course can optimized even further, and can benefit from few refactors, however this is worth celebrating, with only two articles we have created a real-time browser application.
If you want to take it further there's an optional article React.js && Websockets, processing real-time data w/ web threads, where we take the entire hook and move it to a separate web thread, increasing the application performance drastically, not mentioning a non-blocking UI.
Thank you for reading! If you loved this article and share my excitement for Back-end or Desktop development and Machine Learning in JavaScript, you're in for a treat! Get ready for an exhilarating ride with awesome articles and exclusive content on Ko-fi. And that's not all ā I've got Python and Golang up my sleeve too! š Let's embark on this thrilling journey together!
Oh, but wait, there's more! For those of you craving more than just CRUD web apps, we've got an exclusive spot waiting for you in the All Things Data: Data Collection, Science, and Machine Learning in JavaScript, and a Tinsy Bit of Python program. Limited spots available ā don't miss out on the party! Let's geek out together, shall we? š
Top comments (0)