Vibe coding with AI course Β· Module 8: Slack Clone

Analytics Dashboard - Metrics and Charts

4 min read
In this lesson3

The captain wants to know whether Ship's Comms is alive: how many people visit, how many click, and how many do what we actually care about. Without numbers, it's sailing in fog. We'll build an analytics dashboard with stat cards and a chart, a navigation board on the bridge.

What we measure

First, a small glossary. Views are page views, clicks are clicks, and conversions are completed goals, e.g. a sign-up or a purchase. Fewer people take each next step, so conversions are usually the smallest number. That's why next to every number we also show the change compared with the previous period, because a number alone won't tell you whether you're sailing in the right direction.

Charts with Recharts

For charts we'll use the Recharts library, which provides charts as React components. Installation is a single command in the terminal:

npm install recharts

The command adds recharts to the dependencies in package.json, but it doesn't draw anything yet. The library's README also recommends installing the react-is package in the version that matches your React. Then you assemble a line chart from building blocks:

import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';

function ViewsChart() {
  const data = [
    { date: '2024-01', views: 400 },
    { date: '2024-02', views: 300 },
    { date: '2024-03', views: 600 },
    { date: '2024-04', views: 800 }
  ];

  return (
    <LineChart width={600} height={300} data={data}>
      <CartesianGrid strokeDasharray="3 3" />
      <XAxis dataKey="date" />
      <YAxis />
      <Tooltip />
      <Line type="monotone" dataKey="views" stroke="#3b82f6" />
    </LineChart>
  );
}

The data is an array of objects, and Recharts reads their fields by the name given in dataKey. XAxis takes the month from dataKey="date", and Line draws the values from dataKey="views". CartesianGrid adds a dashed grid, Tooltip shows the value on hover, and type="monotone" smooths the line. The data doesn't change, only its picture does. A fixed width={600} easily overflows a phone screen, so to make the chart fill its parent's width, wrap it in ResponsiveContainer. In Next.js with the App Router, put the chart in a component marked 'use client', because Recharts runs in the browser.

Stat cards

Above the chart stand cards with the most important numbers:

function StatsGrid() {
  const stats = [
    { label: 'Total Views', value: '12.5K', change: '+12%', up: true },
    { label: 'Clicks', value: '3.2K', change: '+5%', up: true },
    { label: 'Conversions', value: '450', change: '-2%', up: false }
  ];

  return (
    <div className="grid grid-cols-3 gap-4">
      {stats.map(stat => (
        <div key={stat.label} className="border rounded p-4 bg-white">
          <p className="text-sm text-gray-600">{stat.label}</p>
          <p className="text-3xl font-bold">{stat.value}</p>
          <p className={`text-sm ${stat.up ? 'text-green-600' : 'text-red-600'}`}>
            {stat.change}
          </p>
        </div>
      ))}
    </div>
  );
}

Each card (stat) has four fields: label, value, change and up. When up is true, the change is green (text-green-600), and when it drops, red (text-red-600). A conditional expression in className picks the color, so the data doesn't need to know about colors. In the example the change is hard-coded, but in a real dashboard you compute it from two periods:

const changePercent = Math.round(((current - previous) / previous) * 100);
const up = changePercent >= 0;

For 450 conversions against 460 a month earlier, you get -2%, so up is false. The numbers themselves stay untouched, they only produce two new fields.

You can build a simple bar chart even without a library, from plain div elements. The height of a bar is a percentage of the largest value:

const maxViews = Math.max(...chartData.map(d => d.views));

function barHeight(views: number) {
  return `${(views / maxViews) * 100}%`;
}

The tallest bar gets 100%, the others proportionally less. You assign the result in style={{ height: barHeight(d.views) }}, and the input data stays untouched.

Captain Redbeard's tip: before you draw a chart, ask which decision it should make easier. A pretty curve that leads nowhere is just decoration on the mast.

Remember: a card shows a number and the direction of change, and a chart shows the trend. In the editor below you have an analytics dashboard with stat cards, and the tasks will check the chart library and the stat cards.

Code for this lesson: App.jsx
// Analytics Dashboard - Stats Cards + Bar Chart
import { useState } from 'react';

const stats = [
  { label: 'Total Views', value: '12.5K', change: '+12%', up: true },
  { label: 'Clicks', value: '3.2K', change: '+5%', up: true },
  { label: 'Conversions', value: '450', change: '-2%', up: false }
];

const chartData = [
  { month: 'Jan', views: 400 },
  { month: 'Feb', views: 300 },
  { month: 'Mar', views: 600 },
  { month: 'Apr', views: 800 }
];

export default function App() {
  const maxViews = Math.max(...chartData.map(d => d.views));

  return (
    <div className="min-h-screen bg-gray-100 p-8">
      <div className="max-w-4xl mx-auto">
        <h1 className="text-2xl font-bold mb-6">Analytics Dashboard</h1>

        {/* Stats Cards Grid */}
        <div className="grid grid-cols-3 gap-4 mb-8">
          {stats.map(stat => (
            <div key={stat.label} className="border rounded p-4 bg-white">
              <p className="text-sm text-gray-600">{stat.label}</p>
              <p className="text-3xl font-bold">{stat.value}</p>
              <p className={`text-sm ${stat.up ? 'text-green-600' : 'text-red-600'}`}>
                {stat.change}
              </p>
            </div>
          ))}
        </div>

        {/* Simple Bar Chart */}
        <div className="bg-white border rounded p-6">
          <h2 className="font-semibold mb-4">Views per month</h2>
          <div className="flex items-end gap-4 h-48">
            {chartData.map(d => (
              <div key={d.month} className="flex-1 flex flex-col items-center justify-end">
                <div
                  className="w-full bg-blue-500 rounded-t"
                  style={{ height: `${(d.views / maxViews) * 100}%` }}
                ></div>
                <span className="text-xs text-gray-600 mt-2">{d.month}</span>
                <span className="text-xs font-semibold">{d.views}</span>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Which library do we use for charts in the dashboard?

  2. 2. What fields does a single stats card (stat) have?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Analytics dashboard with cards and a chart

  • Horizontal ordering

    Arrange the command to install the chart library (one line, left to right):

Useful articles