Building a live chart with Deno, WebSockets, Chart.js and Materialize

Introduction

Overview

Prerequisites

Running the demo

git clone git clone https://github.com/bobbyiliev/materialize-tutorials.git
cd materialize-tutorials/mz-deno-live-dashboard
docker-compose build
docker-compose up -d

Materialize setup

CREATE SOURCE score
FROM KAFKA BROKER 'redpanda:9092' TOPIC 'score_topic'
FORMAT BYTES;
CREATE VIEW score_view AS
SELECT
*
FROM (
SELECT
(data->>'user_id')::int AS user_id,
(data->>'score')::int AS score,
(data->>'created_at')::double AS created_at
FROM (
SELECT CAST(data AS jsonb) AS data
FROM (
SELECT convert_from(data, 'utf8') AS data
FROM score
)
)
);
CREATE MATERIALIZED VIEW score_view_mz AS
SELECT
(SUM(score))::int AS user_score,
user_id
FROM score_view GROUP BY user_id;
docker-compose run mzcli
SHOW VIEWS;
-- Output:
-- +-----------------+
-- | score_view |
-- | score_view_mz |
-- +-----------------+
SHOW sources;
-- Output:
-- +-----------------+
-- | score |
-- +-----------------+

Using TAIL

COPY ( TAIL score_view_mz ) TO STDOUT;
COPY ( TAIL score_view_mz WITH (SNAPSHOT = false) ) TO STDOUT;

Deno

import { WebSocketClient, WebSocketServer } from "https://deno.land/x/websocket@v0.1.4/mod.ts";
import { Client } from "https://deno.land/x/postgres/mod.ts";
// Specify your Materialize connection details
const client = new Client({
user: "materialize",
database: "materialize",
hostname: "materialized",
port: 6875,
});
await client.connect();
console.log("Connected to Postgres");
// Start a transaction
await client.queryObject('BEGIN');
// Declare a cursor without a snapshot
await client.queryObject(`DECLARE c CURSOR FOR TAIL score_view_mz WITH (SNAPSHOT = false)`);
const wss = new WebSocketServer(8080);wss.on("connection", async function (ws: WebSocketClient) {
console.log("Client connected");
setInterval(async () => {
const result = await client.queryObject<{ mz_timestamp: string; mz_diff: number, user_id: number, user_score: number}>(`FETCH ALL c`);
for (const row of result.rows) {
let message = { user_id: row.user_id, user_score: row.user_score };
broadcastEvent(message);
}
} , 1000);
});// Broadcast a message to all clients
const broadcastEvent = (message: any) => {
wss.clients.forEach((ws: WebSocketClient) => {
ws.send(JSON.stringify(message));
});
}

Frontend setup

<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div class="w-full mt-10">
<canvas id="myChart"></canvas>
</div>
<script>
const ctx = document.getElementById("myChart");
const myChart = new Chart(ctx, {
type: "bar",
data: {
labels: [ "Player 1", "Player 2", "Player 3", "Player 4", "Player 5", "Player 6" ],
datasets: [
{
label: "# of points",
data: [0, 0, 0, 0, 0, 0],
backgroundColor: [
"rgba(255, 99, 132, 0.2)",
"rgba(54, 162, 235, 0.2)",
"rgba(255, 206, 86, 0.2)",
"rgba(75, 192, 192, 0.2)",
"rgba(153, 102, 255, 0.2)",
"rgba(255, 159, 64, 0.2)",
],
borderColor: [
"rgba(255, 99, 132, 1)",
"rgba(54, 162, 235, 1)",
"rgba(255, 206, 86, 1)",
"rgba(75, 192, 192, 1)",
"rgba(153, 102, 255, 1)",
"rgba(255, 159, 64, 1)",
],
borderWidth: 1,
},
],
},
options: {
scales: {
y: {
beginAtZero: true,
},
},
},
});
webSocket = new WebSocket("ws://127.0.0.1:8080");
webSocket.onmessage = function (message) {
const data = message.data;
const dataObj = JSON.parse(data);
const dataArray = Object.values(dataObj);
console.log(dataArray);
index = dataArray[0] - 1;
myChart.data.datasets[0].data[index] = dataArray[1];
myChart.update();
};
</script>
</body>
</html>

Conclusion

Helpful resources:

Community

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Bobby Iliev

I am a professional System Administrator with a demonstrated history of working in the internet industry. I am a Linux lover