Adding Charts to Django with Chart.js

Last updated April 16th, 2023

In this tutorial, we'll look at how to add interactive charts to the Django with Chart.js. We'll use Django to model and prepare the data and then fetch it asynchronously from our template using AJAX. Finally, we'll look at how to create new Django admin views and extend existing admin templates in order to add custom charts to the Django admin.

Contents

What is Chart.js?

Chart.js is an open-source JavaScript library for data visualization. It supports eight different chart types: bar, line, area, pie, bubble, radar, polar, and scatter. It's flexible and highly customizable. It supports animations. Add best of all, it's easy to use.

To get started, you just need to include the Chart.js script along with a <canvas> node in an HTML file:

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>

<canvas id="chart" width="500" height="500"></canvas>

Then, you can create a chart like so:

let ctx = document.getElementById("chart").getContext("2d");

let chart = new Chart(ctx, {
  type: "bar",
  data: {
     labels: ["2020/Q1", "2020/Q2", "2020/Q3", "2020/Q4"],
     datasets: [
        {
          label: "Gross volume ($)",
          backgroundColor: "#79AEC8",
          borderColor: "#417690",
          data: [26900, 28700, 27300, 29200]
        }
     ]
  },
  options: {
     title: {
        text: "Gross Volume in 2020",
        display: true
     }
  }
});

This code creates the following chart:

See the Pen wvogmJy by Michael Herman (@mjhea0) on CodePen.

CodePen link

To learn more about Chart.js check out the official documentation.

With that, let's look at how to add charts to Django with Chart.js.

Project Setup

Let's create a simple shop application. We'll generate sample data using a Django management command and then visualize it with Chart.js.

Prefer a different JavaScript chart library like D3.js or Chartist? You can use the same approach for adding the charts to Django and the Django admin. You'll just need to adjust how data is formatted in your JSON endpoints.

Workflow:

  1. Fetch data using Django ORM queries
  2. Format the data and return it via a protected endpoint
  3. Request the data from a template using AJAX
  4. Initialize Chart.js and load the data

Start by creating a new directory and setting up a new Django project:

$ mkdir django-interactive-charts && cd django-interactive-charts
$ python3.11 -m venv env
$ source env/bin/activate

(env)$ pip install django==4.2
(env)$ django-admin startproject core .

After that, create a new app called shop:

(env)$ python manage.py startapp shop

Register the app in core/settings.py under INSTALLED_APPS:

# core/settings.py

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "shop.apps.ShopConfig",  # new
]

Create Database Models

Next, create Item and Purchase models:

# shop/models.py

from django.db import models


class Item(models.Model):
    name = models.CharField(max_length=255)
    description = models.TextField(null=True)
    price = models.FloatField(default=0)

    def __str__(self):
        return f"{self.name} (${self.price})"


class Purchase(models.Model):
    customer_full_name = models.CharField(max_length=64)
    item = models.ForeignKey(to=Item, on_delete=models.CASCADE)
    PAYMENT_METHODS = [
        ("CC", "Credit card"),
        ("DC", "Debit card"),
        ("ET", "Ethereum"),
        ("BC", "Bitcoin"),
    ]
    payment_method = models.CharField(max_length=2, default="CC", choices=PAYMENT_METHODS)
    time = models.DateTimeField(auto_now_add=True)
    successful = models.BooleanField(default=False)

    class Meta:
        ordering = ["-time"]

    def __str__(self):
        return f"{self.customer_full_name}, {self.payment_method} ({self.item.name})"
  1. Item represents an item in our store
  2. Purchase represents a purchase (that's linked to an Item)

Make migrations and then apply them:

(env)$ python manage.py makemigrations
(env)$ python manage.py migrate

Register the models in shop/admin.py:

# shop/admin.py

from django.contrib import admin

from shop.models import Item, Purchase


admin.site.register(Item)
admin.site.register(Purchase)

Populate the Database

Before creating any charts, we need some data to work with. I've created a simple command we can use to populate the database.

Create a new folder in "shop" called "management", and then inside that folder create another folder called "commands". Within the "commands" folder, create a new file called populate_db.py.

management
└── commands
    └── populate_db.py

populate_db.py:

# shop/management/commands/populate_db.py

import random
from datetime import datetime, timedelta

import pytz
from django.core.management.base import BaseCommand

from shop.models import Item, Purchase


class Command(BaseCommand):
    help = "Populates the database with random generated data."

    def add_arguments(self, parser):
        parser.add_argument("--amount", type=int, help="The number of purchases that should be created.")

    def handle(self, *args, **options):
        names = ["James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph", "Thomas", "Charles"]
        surname = ["Smith", "Jones", "Taylor", "Brown", "Williams", "Wilson", "Johnson", "Davies", "Patel", "Wright"]
        items = [
            Item.objects.get_or_create(name="Socks", price=6.5), Item.objects.get_or_create(name="Pants", price=12),
            Item.objects.get_or_create(name="T-Shirt", price=8), Item.objects.get_or_create(name="Boots", price=9),
            Item.objects.get_or_create(name="Sweater", price=3), Item.objects.get_or_create(name="Underwear", price=9),
            Item.objects.get_or_create(name="Leggings", price=7), Item.objects.get_or_create(name="Cap", price=5),
        ]
        amount = options["amount"] if options["amount"] else 2500
        for i in range(0, amount):
            dt = pytz.utc.localize(datetime.now() - timedelta(days=random.randint(0, 1825)))
            purchase = Purchase.objects.create(
                customer_full_name=random.choice(names) + " " + random.choice(surname),
                item=random.choice(items)[0],
                payment_method=random.choice(Purchase.PAYMENT_METHODS)[0],
                successful=True if random.randint(1, 2) == 1 else False,
            )
            purchase.time = dt
            purchase.save()

        self.stdout.write(self.style.SUCCESS("Successfully populated the database."))

Run the following command to populate the DB:

(env)$ python manage.py populate_db --amount 1000

If everything went well you should see a Successfully populated the database. message in the console.

This added 8 items and 1,000 purchases to the database.

Prepare and Serve the Data

Our app will have the following endpoints:

  1. chart/filter-options/ lists all the years we have records for
  2. chart/sales/<YEAR>/ fetches monthly gross volume data by year
  3. chart/spend-per-customer/<YEAR>/ fetches monthly spend per customer by year
  4. chart/payment-success/<YEAR>/ fetches yearly payment success data
  5. chart/payment-method/<YEAR>/ fetches yearly payment method data (Credit card, Debit card, Ethereum, Bitcoin)

Before we add the views, let's create a few utility functions that will make it easier to create the charts. Add a new folder in the project root called "utils". Then, add a new file to that folder called charts.py:

# util/charts.py

months = [
    "January", "February", "March", "April",
    "May", "June", "July", "August",
    "September", "October", "November", "December"
]
colorPalette = ["#55efc4", "#81ecec", "#a29bfe", "#ffeaa7", "#fab1a0", "#ff7675", "#fd79a8"]
colorPrimary, colorSuccess, colorDanger = "#79aec8", colorPalette[0], colorPalette[5]


def get_year_dict():
    year_dict = dict()

    for month in months:
        year_dict[month] = 0

    return year_dict


def generate_color_palette(amount):
    palette = []

    i = 0
    while i < len(colorPalette) and len(palette) < amount:
        palette.append(colorPalette[i])
        i += 1
        if i == len(colorPalette) and len(palette) < amount:
            i = 0

    return palette

So, we defined our chart colors and created the following two methods:

  1. get_year_dict() creates a dictionary of months and values, which we'll use to add the monthly data to.
  2. generate_color_palette(amount) generates a repeating color palette that we'll pass to our charts.

Views

Create the views:

# shop/views.py

from django.contrib.admin.views.decorators import staff_member_required
from django.db.models import Count, F, Sum, Avg
from django.db.models.functions import ExtractYear, ExtractMonth
from django.http import JsonResponse

from shop.models import Purchase
from utils.charts import months, colorPrimary, colorSuccess, colorDanger, generate_color_palette, get_year_dict


@staff_member_required
def get_filter_options(request):
    grouped_purchases = Purchase.objects.annotate(year=ExtractYear("time")).values("year").order_by("-year").distinct()
    options = [purchase["year"] for purchase in grouped_purchases]

    return JsonResponse({
        "options": options,
    })


@staff_member_required
def get_sales_chart(request, year):
    purchases = Purchase.objects.filter(time__year=year)
    grouped_purchases = purchases.annotate(price=F("item__price")).annotate(month=ExtractMonth("time"))\
        .values("month").annotate(average=Sum("item__price")).values("month", "average").order_by("month")

    sales_dict = get_year_dict()

    for group in grouped_purchases:
        sales_dict[months[group["month"]-1]] = round(group["average"], 2)

    return JsonResponse({
        "title": f"Sales in {year}",
        "data": {
            "labels": list(sales_dict.keys()),
            "datasets": [{
                "label": "Amount ($)",
                "backgroundColor": colorPrimary,
                "borderColor": colorPrimary,
                "data": list(sales_dict.values()),
            }]
        },
    })


@staff_member_required
def spend_per_customer_chart(request, year):
    purchases = Purchase.objects.filter(time__year=year)
    grouped_purchases = purchases.annotate(price=F("item__price")).annotate(month=ExtractMonth("time"))\
        .values("month").annotate(average=Avg("item__price")).values("month", "average").order_by("month")

    spend_per_customer_dict = get_year_dict()

    for group in grouped_purchases:
        spend_per_customer_dict[months[group["month"]-1]] = round(group["average"], 2)

    return JsonResponse({
        "title": f"Spend per customer in {year}",
        "data": {
            "labels": list(spend_per_customer_dict.keys()),
            "datasets": [{
                "label": "Amount ($)",
                "backgroundColor": colorPrimary,
                "borderColor": colorPrimary,
                "data": list(spend_per_customer_dict.values()),
            }]
        },
    })


@staff_member_required
def payment_success_chart(request, year):
    purchases = Purchase.objects.filter(time__year=year)

    return JsonResponse({
        "title": f"Payment success rate in {year}",
        "data": {
            "labels": ["Successful", "Unsuccessful"],
            "datasets": [{
                "label": "Amount ($)",
                "backgroundColor": [colorSuccess, colorDanger],
                "borderColor": [colorSuccess, colorDanger],
                "data": [
                    purchases.filter(successful=True).count(),
                    purchases.filter(successful=False).count(),
                ],
            }]
        },
    })


@staff_member_required
def payment_method_chart(request, year):
    purchases = Purchase.objects.filter(time__year=year)
    grouped_purchases = purchases.values("payment_method").annotate(count=Count("id"))\
        .values("payment_method", "count").order_by("payment_method")

    payment_method_dict = dict()

    for payment_method in Purchase.PAYMENT_METHODS:
        payment_method_dict[payment_method[1]] = 0

    for group in grouped_purchases:
        payment_method_dict[dict(Purchase.PAYMENT_METHODS)[group["payment_method"]]] = group["count"]

    return JsonResponse({
        "title": f"Payment method rate in {year}",
        "data": {
            "labels": list(payment_method_dict.keys()),
            "datasets": [{
                "label": "Amount ($)",
                "backgroundColor": generate_color_palette(len(payment_method_dict)),
                "borderColor": generate_color_palette(len(payment_method_dict)),
                "data": list(payment_method_dict.values()),
            }]
        },
    })

Notes:

  1. get_filter_options() fetches all the purchases, groups them by year, extracts the year from the time field, and returns them in a list.
  2. get_sales_chart() fetches all the purchases (in a specific year) and their prices, groups them by month, and calculates the monthly sum of prices.
  3. spend_per_customer_chart() fetches all the purchases (in a specific year) and their prices, groups them by month, and calculates the average monthly price.
  4. payment_success_chart() counts successful and unsuccessful purchases in a specific year.
  5. payment_method_chart() fetches all the purchases (in a specific year), groups them by payment method, counts the purchases, and returns them in a dictionary.

Note that every view has the @staff_member_required decorator.

URLs

Create the following app-level URLs:

# shop/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path("chart/filter-options/", views.get_filter_options, name="chart-filter-options"),
    path("chart/sales/<int:year>/", views.get_sales_chart, name="chart-sales"),
    path("chart/spend-per-customer/<int:year>/", views.spend_per_customer_chart, name="chart-spend-per-customer"),
    path("chart/payment-success/<int:year>/", views.payment_success_chart, name="chart-payment-success"),
    path("chart/payment-method/<int:year>/", views.payment_method_chart, name="chart-payment-method"),
]

Then, wire up the app URLs to the project URLs:

# core/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("shop/", include("shop.urls")),  # new
]

Testing

Now that we registered the URLs, let's test the endpoints to see if everything works correctly.

Create a superuser, and then run the development server:

(env)$ python manage.py createsuperuser
(env)$ python manage.py runserver

Then, in your browser of choice, navigate to http://localhost:8000/shop/chart/filter-options/. You should see something like:

{
  "options": [
    2023,
    2022,
    2021,
    2020,
    2019,
    2018
  ]
}

To view the monthly sales data for 2022, navigate to http://localhost:8000/shop/chart/sales/2022/:

{
  "title": "Sales in 2022",
  "data": {
    "labels": [
      "January",
      "February",
      "March",
      "April",
      "May",
      "June",
      "July",
      "August",
      "September",
      "October",
      "November",
      "December"
    ],
    "datasets": [
      {
        "label": "Amount ($)",
        "backgroundColor": "#79aec8",
        "borderColor": "#79aec8",
        "data": [
          134.0,
          104.0,
          97.0,
          100.0,
          144.5,
          118.0,
          116.5,
          153.0,
          130.0,
          143.0,
          149.0,
          114.0
        ]
      }
    ]
  }
}

Create Charts with Chart.js

Moving on, let's wire up Chart.js.

Add a "templates" folder to "shop". Then, add a new file called statistics.html:

<!-- shop/templates/statistics.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Statistics</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
    <script
      src="https://code.jquery.com/jquery-3.6.4.min.js"
      integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8="
      crossorigin="anonymous">
    </script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/bootstrap-grid.min.css">
  </head>
  <body>
    <div class="container">
      <form id="filterForm">
        <label for="year">Choose a year:</label>
        <select name="year" id="year"></select>
        <input type="submit" value="Load" name="_load">
      </form>
      <div class="row">
        <div class="col-6">
          <canvas id="salesChart"></canvas>
        </div>
        <div class="col-6">
          <canvas id="paymentSuccessChart"></canvas>
        </div>
        <div class="col-6">
          <canvas id="spendPerCustomerChart"></canvas>
        </div>
        <div class="col-6">
          <canvas id="paymentMethodChart"></canvas>
        </div>
      </div>
      <script>
        let salesCtx = document.getElementById("salesChart").getContext("2d");
        let salesChart = new Chart(salesCtx, {
          type: "bar",
          options: {
            responsive: true,
            title: {
              display: false,
              text: ""
            }
          }
        });
        let spendPerCustomerCtx = document.getElementById("spendPerCustomerChart").getContext("2d");
        let spendPerCustomerChart = new Chart(spendPerCustomerCtx, {
          type: "line",
          options: {
            responsive: true,
            title: {
              display: false,
              text: ""
            }
          }
        });
        let paymentSuccessCtx = document.getElementById("paymentSuccessChart").getContext("2d");
        let paymentSuccessChart = new Chart(paymentSuccessCtx, {
          type: "pie",
          options: {
            responsive: true,
            maintainAspectRatio: false,
            aspectRatio: 1,
            title: {
              display: false,
              text: ""
            },
            layout: {
              padding: {
                left: 0,
                right: 0,
                top: 0,
                bottom: 25
              }
            }
          }
        });
        let paymentMethodCtx = document.getElementById("paymentMethodChart").getContext("2d");
        let paymentMethodChart = new Chart(paymentMethodCtx, {
          type: "pie",
          options: {
            responsive: true,
            maintainAspectRatio: false,
            aspectRatio: 1,
            title: {
              display: false,
              text: ""
            },
            layout: {
              padding: {
                left: 0,
                right: 0,
                top: 0,
                bottom: 25
              }
            }
          }
        });
      </script>
    </div>
  </body>
</html>

This block of code creates the HTML canvases that Chart.js uses to initialize. We also passed responsive to each charts' options so it adjusts based on the window size.

Add the following script to your HTML file:

<script>
  $(document).ready(function() {
    $.ajax({
      url: "/shop/chart/filter-options/",
      type: "GET",
      dataType: "json",
      success: (jsonResponse) => {
        // Load all the options
        jsonResponse.options.forEach(option => {
          $("#year").append(new Option(option, option));
        });
        // Load data for the first option
        loadAllCharts($("#year").children().first().val());
      },
      error: () => console.log("Failed to fetch chart filter options!")
    });
  });

  $("#filterForm").on("submit", (event) => {
    event.preventDefault();

    const year = $("#year").val();
    loadAllCharts(year)
  });

  function loadChart(chart, endpoint) {
    $.ajax({
      url: endpoint,
      type: "GET",
      dataType: "json",
      success: (jsonResponse) => {
        // Extract data from the response
        const title = jsonResponse.title;
        const labels = jsonResponse.data.labels;
        const datasets = jsonResponse.data.datasets;

        // Reset the current chart
        chart.data.datasets = [];
        chart.data.labels = [];

        // Load new data into the chart
        chart.options.title.text = title;
        chart.options.title.display = true;
        chart.data.labels = labels;
        datasets.forEach(dataset => {
          chart.data.datasets.push(dataset);
        });
        chart.update();
      },
      error: () => console.log("Failed to fetch chart data from " + endpoint + "!")
    });
  }

  function loadAllCharts(year) {
    loadChart(salesChart, `/shop/chart/sales/${year}/`);
    loadChart(spendPerCustomerChart, `/shop/chart/spend-per-customer/${year}/`);
    loadChart(paymentSuccessChart, `/shop/chart/payment-success/${year}/`);
    loadChart(paymentMethodChart, `/shop/chart/payment-method/${year}/`);
  }
</script>

When the page loads this script sends an AJAX request to /chart/filter-options/ to fetch all the active years and loads them into the form.

  1. loadChart loads chart data from the Django endpoint into the chart
  2. loadAllCharts loads all the charts

Note that we used jQuery to handle the AJAX requests for simplicity. Feel free to use the Fetch API instead.

Inside shop/views.py create a new view:

@staff_member_required
def statistics_view(request):
    return render(request, "statistics.html", {})

Don't forget the import:

from django.shortcuts import render

Assign a URL to the view:

# shop/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path("statistics/", views.statistics_view, name="shop-statistics"),  # new
    path("chart/filter-options/", views.get_filter_options, name="chart-filter-options"),
    path("chart/sales/<int:year>/", views.get_sales_chart, name="chart-sales"),
    path("chart/spend-per-customer/<int:year>/", views.spend_per_customer_chart, name="chart-spend-per-customer"),
    path("chart/payment-success/<int:year>/", views.payment_success_chart, name="chart-payment-success"),
    path("chart/payment-method/<int:year>/", views.payment_method_chart, name="chart-payment-method"),
]

Your charts are now accessible at: http://localhost:8000/shop/statistics/.

Add Charts to the Django Admin

With regard to integrating charts into the Django admin, we can:

  1. Create a new Django admin view
  2. Extend an existing admin template

Create a New Django Admin View

Creating a new Django admin view is the cleanest and the most straightforward approach. In this approach, we'll create a new AdminSite and change it in the settings.py file.

First, within "shop/templates", add an "admin" folder. Add a statistics.html template to it:

<!-- shop/templates/admin/statistics.html -->

{% extends "admin/base_site.html" %}
{% block content %}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<script
  src="https://code.jquery.com/jquery-3.6.4.min.js"
  integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8="
  crossorigin="anonymous">
</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/bootstrap-grid.min.css">
<form id="filterForm">
  <label for="year">Choose a year:</label>
  <select name="year" id="year"></select>
  <input type="submit" value="Load" name="_load">
</form>
<script>
  $(document).ready(function() {
    $.ajax({
      url: "/shop/chart/filter-options/",
      type: "GET",
      dataType: "json",
      success: (jsonResponse) => {
        // Load all the options
        jsonResponse.options.forEach(option => {
          $("#year").append(new Option(option, option));
        });
        // Load data for the first option
        loadAllCharts($("#year").children().first().val());
      },
      error: () => console.log("Failed to fetch chart filter options!")
    });
  });

  $("#filterForm").on("submit", (event) => {
    event.preventDefault();

    const year = $("#year").val();
    loadAllCharts(year);
  });

  function loadChart(chart, endpoint) {
    $.ajax({
      url: endpoint,
      type: "GET",
      dataType: "json",
      success: (jsonResponse) => {
        // Extract data from the response
        const title = jsonResponse.title;
        const labels = jsonResponse.data.labels;
        const datasets = jsonResponse.data.datasets;

        // Reset the current chart
        chart.data.datasets = [];
        chart.data.labels = [];

        // Load new data into the chart
        chart.options.title.text = title;
        chart.options.title.display = true;
        chart.data.labels = labels;
        datasets.forEach(dataset => {
            chart.data.datasets.push(dataset);
        });
        chart.update();
      },
      error: () => console.log("Failed to fetch chart data from " + endpoint + "!")
    });
  }

  function loadAllCharts(year) {
    loadChart(salesChart, `/shop/chart/sales/${year}/`);
    loadChart(spendPerCustomerChart, `/shop/chart/spend-per-customer/${year}/`);
    loadChart(paymentSuccessChart, `/shop/chart/payment-success/${year}/`);
    loadChart(paymentMethodChart, `/shop/chart/payment-method/${year}/`);
  }
</script>
<div class="row">
  <div class="col-6">
    <canvas id="salesChart"></canvas>
  </div>
  <div class="col-6">
    <canvas id="paymentSuccessChart"></canvas>
  </div>
  <div class="col-6">
    <canvas id="spendPerCustomerChart"></canvas>
  </div>
  <div class="col-6">
    <canvas id="paymentMethodChart"></canvas>
  </div>
</div>
<script>
  let salesCtx = document.getElementById("salesChart").getContext("2d");
  let salesChart = new Chart(salesCtx, {
    type: "bar",
    options: {
      responsive: true,
        title: {
          display: false,
          text: ""
        }
    }
  });
  let spendPerCustomerCtx = document.getElementById("spendPerCustomerChart").getContext("2d");
  let spendPerCustomerChart = new Chart(spendPerCustomerCtx, {
    type: "line",
    options: {
      responsive: true,
        title: {
          display: false,
          text: ""
        }
    }
  });
  let paymentSuccessCtx = document.getElementById("paymentSuccessChart").getContext("2d");
  let paymentSuccessChart = new Chart(paymentSuccessCtx, {
    type: "pie",
    options: {
      responsive: true,
      maintainAspectRatio: false,
      aspectRatio: 1,
      title: {
        display: false,
        text: ""
      },
      layout: {
        padding: {
          left: 0,
          right: 0,
          top: 0,
          bottom: 25
        }
      }
    }
  });
  let paymentMethodCtx = document.getElementById("paymentMethodChart").getContext("2d");
  let paymentMethodChart = new Chart(paymentMethodCtx, {
    type: "pie",
    options: {
      responsive: true,
      maintainAspectRatio: false,
      aspectRatio: 1,
      title: {
        display: false,
        text: ""
      },
      layout: {
        padding: {
          left: 0,
          right: 0,
          top: 0,
          bottom: 25
        }
      }
    }
  });
</script>
{% endblock %}

Next, create a core/admin.py file:

# core/admin.py

from django.contrib import admin
from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import render
from django.urls import path


@staff_member_required
def admin_statistics_view(request):
    return render(request, "admin/statistics.html", {
        "title": "Statistics"
    })


class CustomAdminSite(admin.AdminSite):
    def get_app_list(self, request, _=None):
        app_list = super().get_app_list(request)
        app_list += [
            {
                "name": "My Custom App",
                "app_label": "my_custom_app",
                "models": [
                    {
                        "name": "Statistics",
                        "object_name": "statistics",
                        "admin_url": "/admin/statistics",
                        "view_only": True,
                    }
                ],
            }
        ]
        return app_list

    def get_urls(self):
        urls = super().get_urls()
        urls += [
            path("statistics/", admin_statistics_view, name="admin-statistics"),
        ]
        return urls

Here, we created a staff protected view called admin_statistics_view. Then, we created a new AdminSite and overrode get_app_list to add our own custom application to it. We provided our application with an artificial view-only model called Statistics. Lastly, we overrode get_urls and assigned a URL to our new view.

Create an AdminConfig inside core/apps.py:

# core/apps.py

from django.contrib.admin.apps import AdminConfig


class CustomAdminConfig(AdminConfig):
    default_site = "core.admin.CustomAdminSite"

Here, we created a new AdminConfig, which loads our CustomAdminSite instead of Django's default one.

Replace the default AdminConfig with the new one in core/settings.py:

INSTALLED_APPS = [
    "core.apps.CustomAdminConfig",  # replaced
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "shop.apps.ShopConfig",
]

Next, update core/urls.py as follows:

# core/urls.py

from django.contrib import admin
from django.urls import path, include

from .admin import admin_statistics_view  # new

urlpatterns = [
    path(   # new
        "admin/statistics/",
        admin.site.admin_view(admin_statistics_view),
        name="admin-statistics"
    ),
    path("admin/", admin.site.urls),
    path("shop/", include("shop.urls")),
]

Navigate to http://localhost:8000/admin/ to view the new Django admin view.

The final result:

new Django admin app

Click on 'View' to see the charts:

new Django admin app charts

Extend an Existing Admin Template

You can always extend admin templates and override the parts you want.

For example, if you wanted to put charts under your shop models, you can do that by overriding the app_index.html template. Add a new app_index.html file to "shop/templates/admin/shop" then add the HTML found here.

Final result:

Django admin template overriding

Conclusion

In this tutorial, you learned how to serve data with Django and then visualize it using Chart.js. We also looked at two different approaches for integrating charts into the Django admin.

Grab the code from the django-interactive-charts repo on GitHub.

Nik Tomazic

Nik Tomazic

Nik is a software developer from Slovenia. He's interested in object-oriented programming and web development. He likes learning new things and accepting new challenges. When he's not coding, Nik's either swimming or watching movies.

Share this tutorial

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.