Build a Real‑Time Dashboard with Deno, Materialize, and Chart.js

This tutorial walks through creating a live dashboard by generating simulated user‑score events, storing them in Redpanda, materializing the data with Materialize, streaming updates via a Deno WebSocket backend, and visualizing the results in real time with Chart.js on the frontend.

21CTO
21CTO
21CTO
Build a Real‑Time Dashboard with Deno, Materialize, and Chart.js

This tutorial demonstrates how to build a real‑time dashboard using Deno, Materialize, WebSockets, and Chart.js.

Overview

The project consists of a simulated service that continuously generates user‑score events, stores them in a Redpanda topic, materializes the data with Materialize, and streams the latest scores to a frontend chart via a Deno WebSocket server.

Prerequisites

Docker

Docker Compose

Running the Demo

Clone the repository and start the containers:

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

After the containers are up, open http://localhost in a browser to view the live chart.

Implementation Setup

Materialize DDL executed by the Deno service:

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;

To verify the views and source, run the Materialize CLI:

docker-compose run mzcli
psql -U materialize -h localhost -p 6875 materialize
SHOW VIEWS;
SHOW sources;

Use TAIL to stream updates:

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

Deno Backend Code

import { WebSocketClient, WebSocketServer } from "https://deno.land/x/[email protected]/mod.ts";
import { Client } from "https://deno.land/x/postgres/mod.ts";

const client = new Client({
  user: "materialize",
  database: "materialize",
  hostname: "materialized",
  port: 6875,
});
await client.connect();
await client.queryObject('BEGIN');
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);
});

const broadcastEvent = (message: any) => {
  wss.clients.forEach((ws: WebSocketClient) => {
    ws.send(JSON.stringify(message));
  });
};

Frontend Setup

The frontend uses plain HTML and Chart.js (no framework). It connects to the Deno WebSocket server and updates the chart in real time.

<!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 } } }
      });
      const webSocket = new WebSocket("ws://127.0.0.1:8080");
      webSocket.onmessage = function (message) {
        const dataObj = JSON.parse(message.data);
        const index = dataObj.user_id - 1;
        myChart.data.datasets[0].data[index] = dataObj.user_score;
        myChart.update();
      };
    </script>
  </body>
</html>

Conclusion

By keeping the Deno application running, it continuously subscribes to Materialize, fetches the latest scores via a TAIL cursor, and pushes updates to the frontend chart, resulting in a fully functional real‑time dashboard.

Author: 华山

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backend DevelopmentReal-time DashboardDenoµWebSocketsChart.jsmaterialize
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.