Django

⌘K
  1. Home
  2. Django
  3. Htmx
  4. beginner tutorial
  5. Live Dashboard

Live Dashboard

Thought for 3 seconds### Tutorial: Building a Simple Product Sales Dashboard with Django and HTMX

In this tutorial, we’ll build a real-time product sales dashboard that displays key business metrics and updates automatically. We’ll keep it simple but effective, focusing on the most important sales data.

Project Overview

We’ll create a dashboard that shows:

  • Total sales and revenue today
  • Top selling products
  • Sales trend over time (line chart)
  • Sales by product category (pie chart)

The dashboard will update automatically every few seconds without page refreshes.

Step 1: Set Up Your Django Project

First, let’s create a new Django project:

# Create a virtual environment
python -m venv venv

# Activate the virtual environment
# On Windows:
venv\Scripts\activate
# On macOS/Linux:
source venv/bin/activate

# Install Django and django-htmx
pip install django django-htmx

# Create a new Django project
django-admin startproject sales_dashboard

# Navigate to the project directory
cd sales_dashboard

# Create a new app
python manage.py startapp dashboard

Step 2: Configure Your Project

Update your settings.py file:

# sales_dashboard/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'dashboard',  # Add your app
    'django_htmx',  # Add django-htmx
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django_htmx.middleware.HtmxMiddleware',  # Add HTMX middleware
]

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],  # Add templates directory
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Step 3: Create Models

Let’s create models for our products and sales:

# dashboard/models.py
from django.db import models
from django.utils import timezone

class Category(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = "Categories"

class Product(models.Model):
    name = models.CharField(max_length=200)
    category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='products')
    price = models.DecimalField(max_digits=10, decimal_places=2)

    def __str__(self):
        return self.name

class Sale(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='sales')
    quantity = models.PositiveIntegerField(default=1)
    timestamp = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return f"{self.quantity} x {self.product.name} at {self.timestamp}"

    @property
    def total_price(self):
        return self.product.price * self.quantity

Now run migrations to create the database tables:

python manage.py makemigrations
python manage.py migrate

Step 4: Create Admin Interface

Let’s set up the admin interface to easily add products and sales:

# dashboard/admin.py
from django.contrib import admin
from .models import Category, Product, Sale

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('name',)

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ('name', 'category', 'price')
    list_filter = ('category',)
    search_fields = ('name',)

@admin.register(Sale)
class SaleAdmin(admin.ModelAdmin):
    list_display = ('product', 'quantity', 'timestamp', 'get_total_price')
    list_filter = ('product__category', 'timestamp')
    date_hierarchy = 'timestamp'

    def get_total_price(self, obj):
        return f"${obj.total_price}"
    get_total_price.short_description = 'Total Price'

Step 5: Create Demo Data Script

Let’s create a management command to generate demo data:

# dashboard/management/commands/generate_demo_data.py
from django.core.management.base import BaseCommand
from django.utils import timezone
from dashboard.models import Category, Product, Sale
import random
from datetime import timedelta

class Command(BaseCommand):
    help = 'Generates demo data for the sales dashboard'

    def handle(self, *args, **kwargs):
        # Create categories
        categories = [
            'Electronics',
            'Clothing',
            'Home & Kitchen',
            'Books',
            'Toys',
        ]

        for cat_name in categories:
            Category.objects.get_or_create(name=cat_name)

        self.stdout.write(self.style.SUCCESS(f'Created {len(categories)} categories'))

        # Create products
        products_data = [
            # Electronics
            {'name': 'Smartphone', 'category': 'Electronics', 'price': 699.99},
            {'name': 'Laptop', 'category': 'Electronics', 'price': 1299.99},
            {'name': 'Headphones', 'category': 'Electronics', 'price': 149.99},
            {'name': 'Tablet', 'category': 'Electronics', 'price': 499.99},

            # Clothing
            {'name': 'T-Shirt', 'category': 'Clothing', 'price': 19.99},
            {'name': 'Jeans', 'category': 'Clothing', 'price': 49.99},
            {'name': 'Sneakers', 'category': 'Clothing', 'price': 79.99},
            {'name': 'Jacket', 'category': 'Clothing', 'price': 89.99},

            # Home & Kitchen
            {'name': 'Coffee Maker', 'category': 'Home & Kitchen', 'price': 79.99},
            {'name': 'Blender', 'category': 'Home & Kitchen', 'price': 49.99},
            {'name': 'Toaster', 'category': 'Home & Kitchen', 'price': 29.99},

            # Books
            {'name': 'Fiction Book', 'category': 'Books', 'price': 14.99},
            {'name': 'Cookbook', 'category': 'Books', 'price': 24.99},
            {'name': 'Self-Help Book', 'category': 'Books', 'price': 19.99},

            # Toys
            {'name': 'Action Figure', 'category': 'Toys', 'price': 12.99},
            {'name': 'Board Game', 'category': 'Toys', 'price': 34.99},
            {'name': 'Puzzle', 'category': 'Toys', 'price': 19.99},
        ]

        for product_data in products_data:
            category = Category.objects.get(name=product_data['category'])
            Product.objects.get_or_create(
                name=product_data['name'],
                defaults={
                    'category': category,
                    'price': product_data['price']
                }
            )

        self.stdout.write(self.style.SUCCESS(f'Created {len(products_data)} products'))

        # Generate sales for the past 7 days
        products = Product.objects.all()
        now = timezone.now()

        # Delete existing sales
        Sale.objects.all().delete()

        # Generate sales for each day
        for days_ago in range(7, -1, -1):  # 7 days ago to today
            date = now - timedelta(days=days_ago)

            # More sales for recent days
            num_sales = random.randint(10, 50) if days_ago < 3 else random.randint(5, 30)

            for _ in range(num_sales):
                product = random.choice(products)
                quantity = random.randint(1, 5)

                # Random time during the day
                hour = random.randint(8, 20)  # Between 8 AM and 8 PM
                minute = random.randint(0, 59)
                second = random.randint(0, 59)

                sale_time = date.replace(hour=hour, minute=minute, second=second)

                Sale.objects.create(
                    product=product,
                    quantity=quantity,
                    timestamp=sale_time
                )

        total_sales = Sale.objects.count()
        self.stdout.write(self.style.SUCCESS(f'Generated {total_sales} sales'))

Run the command to generate demo data:

python manage.py generate_demo_data

Step 6: Create Views

Now, let’s create views for our dashboard:

# dashboard/views.py
from django.shortcuts import render
from django.utils import timezone
from django.db.models import Sum, Count, F
from django.http import HttpResponse
from datetime import timedelta
import json
import random
from .models import Category, Product, Sale

def dashboard(request):
    """Main dashboard view"""
    return render(request, 'dashboard/dashboard.html')

def total_sales_today(request):
    """Return the total number of sales and revenue today"""
    today = timezone.now().date()

    # Get sales for today
    sales = Sale.objects.filter(timestamp__date=today)
    total_count = sales.count()

    # Calculate total revenue
    total_revenue = sales.aggregate(
        total=Sum(F('quantity') * F('product__price'))
    )['total'] or 0

    return render(request, 'dashboard/partials/total_sales.html', {
        'total_count': total_count,
        'total_revenue': total_revenue
    })

def top_products(request):
    """Return the top selling products today"""
    today = timezone.now().date()

    # Get top 5 products by quantity sold today
    top_products = Product.objects.filter(
        sales__timestamp__date=today
    ).annotate(
        total_sold=Sum('sales__quantity')
    ).order_by('-total_sold')[:5]

    return render(request, 'dashboard/partials/top_products.html', {
        'top_products': top_products
    })

def sales_trend(request):
    """Return sales trend for the last 7 days"""
    end_date = timezone.now().date()
    start_date = end_date - timedelta(days=6)  # Last 7 days

    # Prepare data structure
    dates = []
    sales_counts = []
    revenue_data = []

    # Get data for each day
    current_date = start_date
    while current_date <= end_date:
        dates.append(current_date.strftime('%Y-%m-%d'))

        # Get sales for this day
        day_sales = Sale.objects.filter(timestamp__date=current_date)
        sales_counts.append(day_sales.count())

        # Calculate revenue for this day
        day_revenue = day_sales.aggregate(
            total=Sum(F('quantity') * F('product__price'))
        )['total'] or 0
        revenue_data.append(float(day_revenue))

        current_date += timedelta(days=1)

    context = {
        'dates': dates,
        'sales_counts': sales_counts,
        'revenue_data': revenue_data,
    }

    return render(request, 'dashboard/partials/sales_trend.html', context)

def sales_by_category(request):
    """Return sales grouped by product category"""
    today = timezone.now().date()

    # Get sales by category
    categories = Category.objects.all()
    labels = []
    data = []

    for category in categories:
        labels.append(category.name)

        # Calculate total sales for this category today
        category_sales = Sale.objects.filter(
            product__category=category,
            timestamp__date=today
        ).aggregate(
            total=Sum('quantity')
        )['total'] or 0

        data.append(category_sales)

    context = {
        'labels': labels,
        'data': data,
    }

    return render(request, 'dashboard/partials/sales_by_category.html', context)

def add_random_sale(request):
    """Add a random sale (for demo purposes)"""
    products = Product.objects.all()
    if not products:
        return HttpResponse("No products available")

    product = random.choice(products)
    quantity = random.randint(1, 3)

    Sale.objects.create(
        product=product,
        quantity=quantity
    )

    return HttpResponse(f"Added sale: {quantity} x {product.name}")

Step 7: Create URLs

Set up the URL patterns:

# dashboard/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.dashboard, name='dashboard'),
    path('total-sales/', views.total_sales_today, name='total_sales'),
    path('top-products/', views.top_products, name='top_products'),
    path('sales-trend/', views.sales_trend, name='sales_trend'),
    path('sales-by-category/', views.sales_by_category, name='sales_by_category'),
    path('add-random-sale/', views.add_random_sale, name='add_random_sale'),
]
# sales_dashboard/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('dashboard.urls')),
]

Step 8: Create Templates

First, create the necessary directories:

mkdir -p templates/dashboard/partials

Now, let’s create our templates:

<!-- templates/dashboard/dashboard.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Sales Dashboard</title>
    <!-- HTMX -->
    <script src="https://unpkg.com/htmx.org@1.9.6"></script>
    <!-- Chart.js -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <!-- Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        .chart-container {
            position: relative;
            height: 300px;
            width: 100%;
        }
    </style>
</head>
<body class="bg-gray-100">
    <div class="container mx-auto px-4 py-8">
        <header class="mb-8">
            <h1 class="text-3xl font-bold text-gray-800">Sales Dashboard</h1>
            <p class="text-gray-600">Real-time sales data updates every 5 seconds</p>
        </header>

        <!-- Dashboard Grid -->
        <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
            <!-- Total Sales Card -->
            <div class="bg-white rounded-lg shadow-md p-6">
                <h2 class="text-xl font-semibold mb-4">Today's Sales</h2>
                <div id="total-sales" 
                     hx-get="{% url 'total_sales' %}" 
                     hx-trigger="load, every 5s">
                    Loading...
                </div>
            </div>

            <!-- Add Random Sale (Demo) -->
            <div class="bg-white rounded-lg shadow-md p-6">
                <h2 class="text-xl font-semibold mb-4">Demo Controls</h2>
                <button hx-post="{% url 'add_random_sale' %}"
                        hx-swap="none"
                        class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded">
                    Simulate New Sale
                </button>
                <p class="text-sm text-gray-500 mt-2">
                    Click to add a random sale (dashboard will update automatically)
                </p>
            </div>

            <!-- Top Products -->
            <div class="bg-white rounded-lg shadow-md p-6">
                <h2 class="text-xl font-semibold mb-4">Top Selling Products Today</h2>
                <div id="top-products" 
                     hx-get="{% url 'top_products' %}" 
                     hx-trigger="load, every 5s">
                    Loading...
                </div>
            </div>

            <!-- Sales by Category Chart -->
            <div class="bg-white rounded-lg shadow-md p-6">
                <h2 class="text-xl font-semibold mb-4">Sales by Category</h2>
                <div id="sales-by-category" 
                     hx-get="{% url 'sales_by_category' %}" 
                     hx-trigger="load, every 5s">
                    Loading...
                </div>
            </div>

            <!-- Sales Trend Chart -->
            <div class="bg-white rounded-lg shadow-md p-6 md:col-span-2">
                <h2 class="text-xl font-semibold mb-4">Sales Trend (Last 7 Days)</h2>
                <div id="sales-trend" 
                     hx-get="{% url 'sales_trend' %}" 
                     hx-trigger="load, every 5s">
                    Loading...
                </div>
            </div>
        </div>
    </div>
</body>
</html>

Now, let’s create the partial templates that will be loaded via HTMX:

<!-- templates/dashboard/partials/total_sales.html -->
<div class="grid grid-cols-2 gap-4">
    <div>
        <div class="text-sm text-gray-500">Total Orders</div>
        <div class="text-4xl font-bold text-blue-600">{{ total_count }}</div>
    </div>
    <div>
        <div class="text-sm text-gray-500">Total Revenue</div>
        <div class="text-4xl font-bold text-green-600">${{ total_revenue|floatformat:2 }}</div>
    </div>
</div>
<!-- templates/dashboard/partials/top_products.html -->
{% if top_products %}
    <div class="overflow-hidden">
        <table class="min-w-full divide-y divide-gray-200">
            <thead class="bg-gray-50">
                <tr>
                    <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Product</th>
                    <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Category</th>
                    <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Units Sold</th>
                </tr>
            </thead>
            <tbody class="bg-white divide-y divide-gray-200">
                {% for product in top_products %}
                <tr>
                    <td class="px-4 py-2 whitespace-nowrap text-sm font-medium text-gray-900">{{ product.name }}</td>
                    <td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">{{ product.category.name }}</td>
                    <td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">{{ product.total_sold }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </div>
{% else %}
    <p class="text-gray-500">No sales data available for today.</p>
{% endif %}
<!-- templates/dashboard/partials/sales_trend.html -->
<div class="chart-container">
    <canvas id="salesTrendChart"></canvas>
</div>

<script>
    // Destroy existing chart if it exists
    if (window.salesTrendChart) {
        window.salesTrendChart.destroy();
    }

    // Create new chart
    const ctx = document.getElementById('salesTrendChart').getContext('2d');
    window.salesTrendChart = new Chart(ctx, {
        type: 'line',
        data: {
            labels: {{ dates|safe }},
            datasets: [
                {
                    label: 'Orders',
                    data: {{ sales_counts|safe }},
                    borderColor: 'rgba(59, 130, 246, 1)',
                    backgroundColor: 'rgba(59, 130, 246, 0.1)',
                    borderWidth: 2,
                    tension: 0.3,
                    yAxisID: 'y'
                },
                {
                    label: 'Revenue ($)',
                    data: {{ revenue_data|safe }},
                    borderColor: 'rgba(16, 185, 129, 1)',
                    backgroundColor: 'rgba(16, 185, 129, 0.1)',
                    borderWidth: 2,
                    tension: 0.3,
                    yAxisID: 'y1'
                }
            ]
        },
        options: {
            responsive: true,
            maintainAspectRatio: false,
            interaction: {
                mode: 'index',
                intersect: false,
            },
            scales: {
                y: {
                    type: 'linear',
                    display: true,
                    position: 'left',
                    title: {
                        display: true,
                        text: 'Orders'
                    },
                    beginAtZero: true,
                    ticks: {
                        precision: 0
                    }
                },
                y1: {
                    type: 'linear',
                    display: true,
                    position: 'right',
                    title: {
                        display: true,
                        text: 'Revenue ($)'
                    },
                    beginAtZero: true,
                    grid: {
                        drawOnChartArea: false,
                    },
                }
            }
        }
    });
</script>
<!-- templates/dashboard/partials/sales_by_category.html -->
<div class="chart-container">
    <canvas id="categoryChart"></canvas>
</div>

<script>
    // Destroy existing chart if it exists
    if (window.categoryChart) {
        window.categoryChart.destroy();
    }

    // Create new chart
    const ctx = document.getElementById('categoryChart').getContext('2d');
    window.categoryChart = new Chart(ctx, {
        type: 'pie',
        data: {
            labels: {{ labels|safe }},
            datasets: [{
                data: {{ data|safe }},
                backgroundColor: [
                    'rgba(255, 99, 132, 0.7)',
                    'rgba(54, 162, 235, 0.7)',
                    'rgba(255, 206, 86, 0.7)',
                    'rgba(75, 192, 192, 0.7)',
                    'rgba(153, 102, 255, 0.7)',
                    'rgba(255, 159, 64, 0.7)',
                ],
                borderWidth: 1
            }]
        },
        options: {
            responsive: true,
            maintainAspectRatio: false,
            plugins: {
                legend: {
                    position: 'right',
                },
                tooltip: {
                    callbacks: {
                        label: function(context) {
                            const label = context.label || '';
                            const value = context.raw || 0;
                            const total = context.dataset.data.reduce((a, b) => a + b, 0);
                            const percentage = Math.round((value / total) * 100);
                            return `${label}: ${value} units (${percentage}%)`;
                        }
                    }
                }
            }
        }
    });
</script>

Step 9: Create a Superuser

Create a superuser to access the admin panel:

python manage.py createsuperuser

Step 10: Run Your Project

Now, let’s run the development server:

python manage.py runserver

Visit http://127.0.0.1:8000/ in your browser to see your real-time sales dashboard!

Visit http://127.0.0.1:8000/admin/ to access the admin panel where you can manually add products and sales.

How It Works

  1. Initial Load: When you first load the dashboard, HTMX makes separate requests to load each component.
  2. Automatic Updates: HTMX automatically refreshes each component every 5 seconds using hx-trigger="load, every 5s".
  3. Chart Updates: When new data arrives, the charts are destroyed and recreated with the new data.
  4. Demo Button: The “Simulate New Sale” button lets you add random sales to see the dashboard update in real-time.

Extending the Project

Here are some ways you could extend this project:

  1. Date Range Selector: Add the ability to view data from different date ranges.
  2. User Authentication: Add login functionality to restrict dashboard access.
  3. Export Functionality: Add buttons to export data as CSV or PDF.
  4. Inventory Tracking: Add inventory levels and alerts for low stock.
  5. Customer Insights: Add customer data and purchase patterns.

Conclusion

You’ve built a simple but effective real-time sales dashboard with Django, HTMX, and Chart.js! This project demonstrates how to:

  • Create a data model for products and sales
  • Display key business metrics in real-time
  • Visualize sales data with charts
  • Create a responsive dashboard layout with Tailwind CSS
  • Update data in real-time without page refreshes

This dashboard provides a solid foundation that you can build upon for your specific business needs. The real-time updates make it a valuable tool for monitoring sales performance throughout the day.

Happy coding!

How can we help?