在这篇文章中,我将向你展示如何使用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")
还有一些观察:
- 我们通过
optionsdict获得命令行参数。 [csv.DictReader()](https://docs.python.org/3/library/csv.html#csv.DictReader)装入csv文件并将每一行与列名(key)联系起来是非常好的。- Django ORM的
get_or_create()是一个很好的助手,它只在对象不存在的情况下创建它。这使得该脚本 idempotent(我可以再次运行它而不会得到重复的记录)。 - 我们使用超类的
stdout和stderr对象来获得更好的输出格式。
让我们来运行它:
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 ,我创建了一个简单的(基于函数的)视图来检索我们刚刚导入的记录,并为图表建立了x 和y 的值。
顺便说一下,我更喜欢基于函数的视图,而不是基于类的视图,见这个资源,为什么...
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)变量来填充labels 和data 属性。
请注意,通常我们会在一个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 ,我看到了这个--很好!

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

用鼠标悬停在条形图上,你会得到漂亮的工具提示。
练习结束
很好!有两个高峰,那是什么?原来那两个月我们参加了一些大规模的Humble Bundle促销活动,我们提供Bite练习包。
很多人在那时注册了平台,并开始兑换他们的Bite代币,从而给Bite编码。利用数据可视化看到这些趋势真的很酷。
总的来说,我们被每个月解决> 1K个练习的稳定速度吓到了!
所以,我希望这能给你一个小模板,你可以用Django轻松建立自己的图表。