DEV Community

Cover image for Django Charts via DRF - Step-by-Step Tutorial
Sm0ke
Sm0ke

Posted on • Originally published at blog.appseed.us

Django Charts via DRF - Step-by-Step Tutorial

Hello Coders!

Django is a powerful web framework built with Python that you can use to create fully functional web applications or web APIs. This article explains step-by-step how to showcase three different charts (pie, line, and bar chart) using Django, Django REST Framework, and Chart.js. The content presents all layers and concepts (API, JS, Html) and also provides the link to the source code, saved on Github.

Thanks for reading! Content provided by App Generator.

  • 👉 Setup the Django project
  • 👉 Writing the app models
  • 👉 Load sample data
  • 👉 Code the serializers
  • 👉 Writing the API via DRF
  • 👉 Code the Javascript part
  • 👉 Update Configuration and routing


Django Charts via DRF and Chart.js - Cover Django.


✨ Setup the project

Before going, make sure that, first of all, let’s set up the project. Feel free to use your favorite python environment management tool. I’ll be using virtualenv here.

$ virtualenv env
$ source env/bin/activate
Enter fullscreen mode Exit fullscreen mode

After that, we install the libraries we’ll be using for the development and create the project.

$ pip install django 
$ pip install djangorestframework 
$ pip install djangorestframework-simplejwt 
$ pip install django-import-export
Enter fullscreen mode Exit fullscreen mode

Create the project

$ django-admin startproject core
Enter fullscreen mode Exit fullscreen mode

We’ll first create an app that will contain all the project-specific features.

$ django-admin startapp apps
Enter fullscreen mode Exit fullscreen mode

After creating, delete all files and folders except __init__.py, views.py, models.py and apps.py. Then open the settings file containing Django configurations and add core to the INSTALLED_APPS:

   # core/settings.py
    ...
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'apps'
Enter fullscreen mode Exit fullscreen mode

Great! Ensure that the project is well set up by running the following commands:

$ python manage.py makemigrations
$ python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

After that, run the following command to start the server.

$ python manage runserver
Enter fullscreen mode Exit fullscreen mode

If you access localhost:8000, we should see the Django default splash screen:

Django Charts via DRF - Default Django Screen


✨ Writing the models

This project will contain two models:

  • The User model.
  • And the Product model.

Following the figure, we notice that the models have the same fields as created and updated. Before creating the application for each feature (user and product), let’s create a base model that will be used by the User model and the Product model.

In the apps/models.py, add the following content.

from django.db import models
from django.utils import timezone

class BaseModel(models.Model):
   created = models.DateTimeField(default=timezone.now)
   updated = models.DateTimeField(auto_now=True)

   class Meta:
       abstract = True
Enter fullscreen mode Exit fullscreen mode

The abstract=True makes sure that no migrations are generated for this model. Let’s move to write the User model.


👉 User model

Before writing the model, we need to create a Django application. In the apps directory, enter the following command.

$ django-admin startapp user
Enter fullscreen mode Exit fullscreen mode

Once it’s done, rewrite the apps/user/apps.py file.

from django.apps import AppConfig

class UserConfig(AppConfig):
   default_auto_field = 'django.db.models.BigAutoField'
   name = 'apps.user'
   label = 'apps_user'
Enter fullscreen mode Exit fullscreen mode

And we can register the application in the settings.py file now.

INSTALLED_APPS = [
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',

   'apps',                       # <-- OLD    
   'apps.user',                  # <-- NEW    

   'import_export',
   'rest_framework'
] 
Enter fullscreen mode Exit fullscreen mode

Let’s write the User model now based on the BaseModel class.

from django.contrib.auth.models import AbstractUser
from django.db import models

from apps.models import BaseModel  

class User(AbstractUser, BaseModel): 

   has_purchased = models.BooleanField(default=False)
Enter fullscreen mode Exit fullscreen mode

Django provides a class called AbstractUser that comes with the following fields.

username = models.CharField(
       _("username"),
       max_length=150,
       unique=True,
       help_text=_(
           "Required. 150 characters or fewer."
       ),
       validators=[username_validator],
       error_messages={
           "unique": _("A user with that username already exists."),
       },
   )
   first_name = models.CharField(_("first name"), max_length=150, blank=True)
   last_name = models.CharField(_("last name"), max_length=150, blank=True)
   email = models.EmailField(_("email address"), blank=True)
   is_staff = models.BooleanField(
       _("staff status"),
       default=False,
       help_text=_("Designates whether the user can log into this admin site."),
   )
   is_active = models.BooleanField(
       _("active"),
       default=True,
       help_text=_(
           "Designates whether this user should be treated as active. "
       ),
   )
   date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
Enter fullscreen mode Exit fullscreen mode

It’s very effective for what we want here. We are also adding a new field has_purchased to track a user who has made at least a purchase on the API. Before running the migrations and committing the changes to the database, let’s add in the settings.py a configuration for the AUTH_USER_MODEL that will be used by Django.

...
AUTH_USER_MODEL = "apps_user.User"
...
Enter fullscreen mode Exit fullscreen mode

Now run the following commands:

$ python manage.py makemigrations
$ python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

With the User model added, let’s write a custom admin class to allow data importation with files. django-import-export is a Django application and library for importing and exporting data with included admin integration.

In the apps/user/admin.py, add the following content:

from django.contrib import admin
from import_export import resources
from import_export.admin import ImportMixin

from apps.user.models import User

class UserResource(resources.ModelResource):
   class Meta:
       model = User
       fields = ('id', 'username', 'email', 'has_purchased', 'created', 'updated')

@admin.register(User)
class UserAdmin(ImportMixin, admin.ModelAdmin):
   resource_class = UserResource
   list_display = ('id', 'username', 'email', 'has_purchased', 'created', 'updated')
   list_filter = ('has_purchased',)
   search_fields = ('username', 'email')
Enter fullscreen mode Exit fullscreen mode

A resource defines how objects are mapped to their import and export representations and handle importing and exporting data. In this filed, we declare the UserResource class containing the model and the fields needed when data is imported.

The UserAdmin class also takes the fields displayed, the field used for filtering data, and the fields used for search.


👉 Product Model

Before writing the Product model, let’s create a Django application called product.

$ django-admin startapp product 
Enter fullscreen mode Exit fullscreen mode

After the application has been created, modify the apps.py file in the product directory.

from django.apps import AppConfig  

class ProductConfig(AppConfig):
   default_auto_field = 'django.db.models.BigAutoField'
   name = 'apps.product'
   label = 'apps_product' 
Enter fullscreen mode Exit fullscreen mode

And then, let’s register the application in the settings.py file.

INSTALLED_APPS = [
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',

   'apps',
   'apps.product',               # <--- NEW 
   'apps.user',

   'import_export',
   'rest_framework'
] 
Enter fullscreen mode Exit fullscreen mode

We can now write the Product model.

from django.db import models

from apps.models import BaseModel

class Product(BaseModel):
   name = models.CharField(max_length=255)
   description = models.TextField()
   price = models.DecimalField(max_digits=10, decimal_places=2)
   quantity_sold = models.IntegerField(default=0)

   def __str__(self):
       return self.name
Enter fullscreen mode Exit fullscreen mode

We will also write a custom admin class for the Product model. In the apps/product/admin.py, add the following content:

from django.contrib import admin
from import_export import resources
from import_export.admin import ImportMixin

from apps.product.models import Product

class ProductResource(resources.ModelResource):
   class Meta:
       model = Product
       fields = ('id', 'name', 'price', 'description', 'created')

@admin.register(Product)
class ProductAdmin(ImportMixin, admin.ModelAdmin):
   resource_class = ProductResource
   list_display = ('id', 'name', 'price', 'description', 'created', 'updated')
   list_filter = ('created',)
   search_fields = ('name',)
Enter fullscreen mode Exit fullscreen mode

Well, with both product admin and user admin written, we can now move to load the sample data in the database using the admin section (import/export feature).


✨ Load Sample Data

Django provides an admin dashboard that you can use to manage resources (models, users, groups) from our Django project. But before accessing the dashboard, you need admin credentials.

In the shell, enter the following command to create a new superuser.

$ django-admin createsuperuser
Enter fullscreen mode Exit fullscreen mode

Once the superuser is created, we can access the admin section of our project localhost:admin/. After you have successfully logged in, you’ll see a similar page.

Django Charts via DRF - Admin Section


Let’s upload data for the users first. Clicking on Add, you’ll be redirected to the following page.


Django Charts via DRF - Load User's Data.

You’ll normally see the first user that is the superuser you’ve just created. Great, now click on import. You’ll have to import a CSV file. You can find an example of this file here.

Once the file is imported, click on Confirm Import.

Django Charts via DRF - Confirm Import Data

Your database is now filled with some users' data 🚀. The next step is to load the products data using the same import feature over the Products using the sample data.


✨ Writing serializers

Serializer allows us to convert Django data structures such as query sets or model instances in Python native objects that can be easily converted to JSON/XML format. Let’s start by writing a serializer for the User model.

👉 User serializer

Inside the apps/user, create a file called serializers.py. This file will contain a class called UserSerializer.

from rest_framework import serializers

from apps.user.models import User

class UserSerializer(serializers.ModelSerializer):

   class Meta:
       model = User
       fields = ('id', 'username', 'email', 'has_purchased')
Enter fullscreen mode Exit fullscreen mode

The ModelSerializer helps write serializers based on models very easier. It automatically matches the fields and their types and even adds some automatic validations.


👉 Product serializer

Inside the apps/product/ directory, create a file called serializers.py. This file will contain a model serializer called ProductSerializer.

from rest_framework import serializers

from apps.product.models import Product  

class ProductSerializer(serializers.ModelSerializer):
   class Meta:
       model = Product
       fields = ('id', 'name', 'price', 'description', 'created', 'updated')
Enter fullscreen mode Exit fullscreen mode

Great! Now that we have serializers, we can write viewsets.


✨ Writing viewsets

A viewset is a class-based view, able to handle all of the basic HTTP requests: GET, POST, PUT, DELETE without hard coding any of the logic. We are using viewsets here to configure the routing for the API easily.

The /user/ viewset should allow the following structure.

Django Charts via DRF - Users Viewsets


And the product resource will have the following structure.

Django Charts via DRF - Product Viewsets


👉 Writing User viewset

Inside the apps/user/ directory, create a file called viewsets.py. The file will contain a class called UserViewSet.

import calendar

from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
from rest_framework.decorators import action

from apps.user.models import User
from apps.user.serializers import UserSerializer

class UserViewSet(viewsets.ModelViewSet):
   http_method_names = ['get']
   queryset = User.objects.all()
   serializer_class = UserSerializer

   @action(detail=False, methods=['get'], url_path='stats/purchased')
   def purchase_stats(self, request, *args, **kwargs):
       users = User.objects.filter(has_purchased=True)
       return Response({
           'total_users': User.objects.count(),
           'total_purchased': users.count(),
           'purchased_percentage': round(users.count() / User.objects.count() * 100, 2)
       }, status=status.HTTP_200_OK)


   @action(detail=False, methods=['get'], url_path='stats/users_created')
   def user_created_per_month(self, request, *args, **kwargs):
       users = User.objects.all()
       months = []
       for user in users:
           months.append(user.created.date().month)
       months = list(set(months))
       months.sort()
       data = []
       for month in months:
           data.append({
               'month': calendar.month_name[month],
               'count': User.objects.filter(created__month=month).count()
           })
       return Response(data, status=status.HTTP_200_OK)
Enter fullscreen mode Exit fullscreen mode

In this viewset, we are allowing only GET requests. And we are also adding custom actions to the viewset such as purchase_stats and user_created_per_month. These actions compute and return some useful stats.

Let’s do the same for the ProductViewSet.


👉 Writing Products viewset

Inside the apps/product directory, create a file called viewsets.py. This file will contain the ProductViewSet class.

import calendar

from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
from rest_framework.decorators import action

from apps.product.models import Product
from apps.product.serializers import ProductSerializer


class ProductViewSet(viewsets.ModelViewSet):
   http_method_names = ['get']
   queryset = Product.objects.all()
   serializer_class = ProductSerializer

   @action(detail=False, methods=['get'], url_path='sales')
   def sales_over_month(self, request, *args, **kwargs):
       products = Product.objects.all()
       months = []
       for product in products:
           months.append(product.created.date().month)
       months = list(set(months))
       months.sort()
       data = []
       for month in months:
           data.append({
               'month': calendar.month_name[month],
               'count': Product.objects.filter(created__month=month).count()
           })
       return Response(data, status=status.HTTP_200_OK)
Enter fullscreen mode Exit fullscreen mode

Great! With the view sets ready, we can register these view sets to create endpoints and start making requests.


✨ Writing API Endpoints

As we are working with viewsets, we can automatically register the API endpoints using routers.

Inside the apps directory, create a file called routers.py.

from rest_framework import routers

from apps.user.viewsets import UserViewSet
from apps.product.viewsets import ProductViewSet

router = routers.SimpleRouter()  

router.register('user', UserViewSet)
router.register('product', ProductViewSet)

urlpatterns = [
   *router.urls,
]
Enter fullscreen mode Exit fullscreen mode

We need then to register this file in the urls.py file of the project. And then these endpoints will be accessible even via the browser.

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

urlpatterns = [
   path('admin/', admin.site.urls),
   path("api/", include(("apps.routers", "api"), namespace="api")),
] 
Enter fullscreen mode Exit fullscreen mode

Great! After that, make sure the server is running because we will start making some requests.


✨ Integrating Chart.JS

Chart.js is a JavaScript library that makes working and building charts easier. In this project, we’ll use this library to display line, bar and pie charts.

The data displayed on the charts will come from the REST API we’ve just built and we’ll be using Axios to fetch data from the server.

But first, let’s make some configurations for the Django templating system and the static file such as the JavaScript file that will contain the logic for data fetching and charts configuration.


👉 Configuration

First of all, make sure you have the following configuration for TEMPLATES in the settings.py file.

TEMPLATES = [
   {
       'BACKEND': 'django.template.backends.django.DjangoTemplates',
       'DIRS': [os.path.join(BASE_DIR, 'apps/templates')],
       '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',
           ],
       },
   },
] 
Enter fullscreen mode Exit fullscreen mode

The templates will be created in the apps/template directory. Also, we need the following configuration for static files.

STATIC_URL = 'static/'

STATICFILES_DIRS = [
   os.path.join(BASE_DIR, 'static'),
]
Enter fullscreen mode Exit fullscreen mode

👉 Index.html - Product HOMEpage

In the apps/templates, create a file called index.html. We’ll just add basic imports for bootstrap, axios and chartjs.

<html lang="en"> 
<head>
<meta charset="utf-8"> 

<!-- Chart JS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css">

<!-- Chart JS -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<!-- Axios -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

<title>Stats</title>
</head>
<body> 

<h1 class="text-center">Product Stats</h1>

<div class="row">

    <!-- LINE Chart -->
    <div class="col-6 mr-2">
        <h2 class="text-center">
            User created over months
        </h2>
        <canvas id="user-created"></canvas>
     </div>

    <!-- Bar Chart -->
    <div class="col-6 ml-2">
        <h2 class="text-center">
            Product sales over months
        </h2>
        <canvas id="product-sales"></canvas>
    </div>

    <!-- PIE Chart -->    
    <div class="col-4 mx-auto">
        <h2 class="text-center">
            Percentage of users who bought a product
        </h2>
        <canvas id="user-purchased"></canvas>
     </div> 

</div>
Enter fullscreen mode Exit fullscreen mode

Once we have a minimum HTML file, we need to code the JS code that consumes the API and injects the data into the charts:


👉 Index.JS - Charts and API

At the root of the project, create a directory called static. Inside this directory create another directory called js. This directory will contain the static JavaScript files. And finally, create a index.js file in the newly-created directory.

Let’s write the steps needed for fetching and configuring charts.

  • Adding a function to fetch data and return a response
  • Retrieve data from each endpoint
  • Initialize the data with labels
  • Initialize the chart configuration
  • And finally, create a chart in the DOM.

Let's break down each step and visualize the necessary code to properly render our beautiful charts.

1# - Adding a function to fetch data and return a response

async function getData(url) {
   const res = await axios.get(url);

   return await res.data;
Enter fullscreen mode Exit fullscreen mode

2# - Retrieve data from each endpoint

let userPurchased = {};
let productSales = [];
let userCreated = [];

// Fetching data for users that have purchased products

userPurchased = getData('/api/user/stats/purchased');

// Fetching data for users created over month

userCreated = getData('/api/user/stats/users_created');

// Fetching data for products sold over month

productSales = getData('/api/product/sales');
Enter fullscreen mode Exit fullscreen mode

3# - Inject the data into Charts

Let’s initialize the data with labels, initialize the chart configuration and finally create a chart in the DOM for the userCreate for example.

userCreated.then((response) => {
   const dataUserCreated = {
       labels: response.map((data) => data.month),
       datasets: [{
           label: 'Users that have purchased products',
           backgroundColor: 'rgb(255, 99, 132)',
           borderColor: 'rgb(255, 99, 132)',
           data: response.map((data) => data.count),
       }]
   };

   const configUserCreated = {
       type: 'line',
       data: dataUserCreated,
       options: {}
   };

   // Creating new chart

   new Chart(
       document.getElementById('user-created'),
       configUserCreated
   );
}) 
Enter fullscreen mode Exit fullscreen mode

And let’s do the same for userPurchased and the productSales.

productSales.then((response) => {
   const dataProductSales = {
       labels: response.map((data) => data.month),
       datasets: [{
           label: 'Products sold',
           data: response.map((data) => data.count),
           borderWidth: 1,
           backgroundColor: 'rgb(255, 99, 132)',
       }]
   };

   const configProductSales = {
       type: 'bar',
       data: dataProductSales,
       options: {
           scales: {
               y: {
                   beginAtZero: true
               }
           }
       },
   };

   new Chart(
       document.getElementById('product-sales'),
       configProductSales
   );
});
Enter fullscreen mode Exit fullscreen mode

userPurchased.then((response) => {
   const dataUserPurchased = {
       labels: [
           'Total Users',
           'Total Purchased',
           'Purchased percentage'
       ],
       datasets: [{
           label: 'Users created',
           data: Object.values(response),
           backgroundColor: [
               'rgb(255, 99, 132)',
               'rgb(54, 162, 235)',
               'rgb(255, 205, 86)'
           ],
           hoverOffset: 4
       }]
   };


   const configUserPurchased = {
       type: 'pie',
       data: dataUserPurchased,
   };


   new Chart(
       document.getElementById('user-purchased'),
       configUserPurchased
   );
});
Enter fullscreen mode Exit fullscreen mode

Once the JS part is fully coded, the last step is to update the product routing and start using the project.

In the apps directory, create a views.py file, and add the following content:

from django.template import loader
from django.shortcuts import render
def index(request):
   context = {"segment": "index"}

   return render(request, "index.html", context) 
Enter fullscreen mode Exit fullscreen mode

And add the urls.py to the apps directory with the following content.

from django.urls import path

from . import views

urlpatterns = [
   # The home page
   path("", views.index, name="home"),
] 
Enter fullscreen mode Exit fullscreen mode

And register the apps/urls.py in the project urls.py file.

..
urlpatterns = [
   path('admin/', admin.site.urls),
   path('', include('apps.urls')),
   path("api/", include(("apps.routers", "api"), namespace="api")),
]
.. 
Enter fullscreen mode Exit fullscreen mode

Make sure the server is running then and visit the app in the browser. At this point, we should see the three charts beautifully rendered on the home page.


Django Charts via DRF and Charts.JS: Bar, Line, and Pie Charts.


✨ Conclusion

In this article, we’ve learned to build an API that serves statistics using Django and Django REST Framework (DRF). We’ve also created an HTML template to display data served by the REST API using Chart.js and Axios.

Thanks for reading! For more resources, feel free to access:

Top comments (7)

Collapse
 
crearesite profile image
WebsiteMarket

Nice breakdown. Ty!

Collapse
 
sm0ke profile image
Sm0ke

🚀🚀

Collapse
 
dailyr profile image
Daily

Great!

Collapse
 
sm0ke profile image
Sm0ke

Ty!

Collapse
 
uithemes profile image
ui-themes

Thanks for writing/sharing!

Collapse
 
sm0ke profile image
Sm0ke

🚀🚀

Collapse
 
aaravrrrrrr profile image
Aarav Reddy

Thanks for writing