如何使用Django和Chart.js制作一个漂亮的图表(附实例)

672 阅读4分钟

在这篇文章中,我将向你展示如何使用Django + Chart.js制作一个漂亮而简单的图表。

这个项目的代码在这里

获取数据

我们将绘制我们平台上每月完成的Bite练习的数量。

为此,我使用这个方便的Postgres命令从我们数据库中的相应表中导出了实时数据:

postgres@0:pybites> \copy (select * from bites_table) to '~/Downloads/bites.csv' WITH (FORMAT CSV, HEADER)

这将把表的内容导出为csv文件。

设置Django

接下来我们将制作一个Django项目和应用程序,这样我们就可以和这个帖子分享了:

$ mkdir bite_stats && cd $_
√ bite_stats  $ python3.10 -m venv venv && source venv/bin/activate
(venv) √ bite_stats  $ pip install django python-dateutil  # latter for date parsing
...
(venv) √ bite_stats  $ django-admin startproject mysite .
(venv) √ bite_stats  $ django-admin startapp stats
(venv) √ bite_stats  $ tree
.
├── manage.py
├── mysite
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── stats
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── migrations
    │   └── __init__.py
    ├── models.py
    ├── tests.py
    └── views.py

3 directories, 13 files

注意startproject 命令的尾部点(.)。我在Django中总是这样做,以免得到额外的嵌套目录。

创建一个模型(数据库表)

接下来让我们制作一个模型,用来保存我们的图表的统计信息。我让它非常简单,因为这篇文章的目的是制作一个简单的图表:

from django.db import models


class BiteStat(models.Model):
    exercise = models.PositiveSmallIntegerField()  # 0 to 32767
    completed = models.DateField()  # I don't care about time here
    level = models.PositiveSmallIntegerField(null=True, blank=True)  # optional, not every Bite has user feedback 

我把stats 应用程序添加到我的INSTALLED_APPS ,在settings.py ,然后运行python manage.py makemigrations ,制作迁移文件,然后python manage.py migrate ,把它(以及所有其他待定的Django迁移)同步到数据库。

注意我们只是使用了Django开箱即用的标准sqlite3数据库。

另外注意我没有更新秘钥和其他环境变量,因为这是一个玩具应用的例子。请看这个视频,如何在一个普通的Django项目中做到这一点。

Django导入器命令

接下来让我们用Django命令把统计表csv加载进来。

首先建立所需的目录结构:

(venv) √ bite_stats  $ mkdir -p stats/management/commands

然后在该目录下制作一个模块。你给它起的名字将成为manage.py 的新开关,我打算用stats/management/commands/import_stats.py ,这样我就可以像这样运行它。python manage.py import_stats

根据文档,我们需要子类BaseCommand ,并实现handle() 方法。

实际上,我在这里做了一个完整的Django命令的演练。

我正在添加一个命令行参数-c (--csv)来指向我电脑上的csv文件。

注意,我使用python-dateutil将日期字符串解析为一个日期时间对象,我需要将没有用户级别的行转换为0 ,以保持与列类型的一致性:

import csv

from django.core.management.base import BaseCommand
from dateutil.parser import parse

from stats.models import BiteStat


class Command(BaseCommand):
    help = 'Import bite exercise stats'

    def add_arguments(self, parser):
        parser.add_argument('-c', '--csv', required=True)

    def handle(self, *args, **options):
        file = options["csv"]
        with open(file) as f:
            reader = csv.DictReader(f)
            for row in reader:
                completed = row["first_completed"]
                if not completed:
                    continue

                level = row["user_level"]
                if not level:
                    level = 0

                date = parse(completed)
                stat, created = BiteStat.objects.get_or_create(
                    exercise=row["bite_id"],
                    completed=date,
                    level=level,
                )

                if created:
                    self.stdout.write(f"{stat} created")
                else:
                    self.stderr.write(f"{stat} already in db")

还有一些观察:

  • 我们通过options dict获得命令行参数。
  • [csv.DictReader()](https://docs.python.org/3/library/csv.html#csv.DictReader)装入csv文件并将每一行与列名(key)联系起来是非常好的。
  • Django ORM的get_or_create() 是一个很好的助手,它只在对象不存在的情况下创建它。这使得该脚本 idempotent(我可以再次运行它而不会得到重复的记录)。
  • 我们使用超类的stdoutstderr 对象来获得更好的输出格式。

让我们来运行它:

python manage.py import_stats -c ~/Downloads/bites.csv

在我写这篇文章的时候,这个数据在终端上滚动了好一会儿,在我们的平台上完成了很多Bites的工作。🎉

现在让我们用Chart.js在一个简单的视图中绘制这些数据 ...

创建一个通往新视图的路由

我首先创建一个视图和路由:

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

from stats import views

urlpatterns = [
    path('', views.index),  # new
    path('admin/', admin.site.urls),
]

为了简单起见,我只是在主路由器(urls.py )中做这个,而不是在应用层面上创建一个新的。

创建一个视图来获取数据并链接到一个模板

stats/views.py ,我创建了一个简单的(基于函数的)视图来检索我们刚刚导入的记录,并为图表建立了xy 的值。

顺便说一下,我更喜欢基于函数的视图,而不是基于类的视图,见这个资源,为什么...

from collections import Counter
from math import ceil

from django.shortcuts import render

from stats.models import BiteStat


def index(request):
    stats = BiteStat.objects.order_by('completed')

    data = Counter()
    for row in stats:
        yymm = row.completed.strftime("%Y-%m")
        data[yymm] += 1

    # unpack dict keys / values into two lists
    labels, values = zip(*data.items())

    context = {
        "labels": labels,
        "values": values,
    }
    return render(request, "graph.html", context)

对于计数来说,collections.Counter() 是标准库的主要内容,zip(*list_of_tuples) 是一个很好的方法,可以将数值解压到labels +values 的列表中。

使用Chart.js创建带有图形的模板

我渲染一个我们接下来要创建的graph.html 模板。我把这个模板放在mysite/templates ,一个我创建的目录。

从文档页面中提取了基本的例子,使用其CDN链接到库,并使用Django的模板语言按照从视图传入的(context)变量来填充labelsdata 属性。

请注意,通常我们会在一个base.html 中抽象出重复的模板代码,并使用继承。我只是用一个模板来保持简单:

<!DOCTYPE html>
<html>
<head>
<title>Bite exercise stats</title>
</head>
<body>

  <canvas id="myChart" width="800" height="400"></canvas>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <script>
  const ctx = document.getElementById('myChart').getContext('2d');
  const myChart = new Chart(ctx, {
      type: 'bar',
      data: {
        labels : [{% for item in labels %}"{{ item }}",{% endfor %}],
          datasets: [{
            label: "Bite exercises complete per month",
            data : [{% for item in values %}{{ item }},{% endfor %}],
            backgroundColor: 'rgba(75, 192, 192, 0.2)',
            borderColor: 'rgba(75, 192, 192, 1)',
            borderWidth: 1
          }]
      },
      options: {
          scales: {
              y: {
                  beginAtZero: true
              }
          }
      }
  });
  </script>

</body>
</html>

DIRS 为了让Django找到这个模板,我把模板目录的路径放到了TEMPLATES 列表中的settings.py

TEMPLATES = [    ...    'DIRS': [Path(BASE_DIR) / 'mysite' / 'templates'],
    ...

再一次在这个 repo 中查看这个项目的源代码。

结果图

现在让我们运行python manage.py runserver ,看看它是否有效...

导航到 localhost:8000 ,我看到了这个--很好!

Screenshot 2022 06 14 at 11.15.42

自从我们开始使用这个平台以来,每个月完成的咬合练习。

Screenshot 2022 06 14 at 11.30.15

用鼠标悬停在条形图上,你会得到漂亮的工具提示。

练习结束

很好!有两个高峰,那是什么?原来那两个月我们参加了一些大规模的Humble Bundle促销活动,我们提供Bite练习包。

很多人在那时注册了平台,并开始兑换他们的Bite代币,从而给Bite编码。利用数据可视化看到这些趋势真的很酷。

总的来说,我们被每个月解决> 1K个练习的稳定速度吓到了!

所以,我希望这能给你一个小模板,你可以用Django轻松建立自己的图表。