I'm trying to understand if it it's possible to incorporate dynamic data into a Django Chart JS architecture. I went through a couple of tutorials and ultimately got Django to work with ChartJS and it's very good when I'm able to hard code values and then display the related graphs. What I'm ultimately trying to do is this same exercise with dynamic data from my database. I found this identical question in SO, https://stackoverflow.com/questions/47575896/dynamic-chart-using-django-and-chart-js#= but it wasn't answered by anyone. This is exactly what I'm trying to achieve. I've explore serializers a bit, do I need to serialize the data first? Thanks in advance for your thoughts and feedback.
Per the feedback, I have added context to the chart in question but the data is still not coming through. Here is my view:
class ChartView(LoginRequiredMixin, TemplateView):
model = Myobject
template_name = 'director/chart.html'
def get_context_data(self, **kwargs):
context = super(ChartView, self).get_context_data(**kwargs)
myobject = Myobject.objects.filter(field__in=self.request.user.userprofile.field.all())
print(myobject)
context['myobject'] = myobject
return context
I'm just getting a blank screen, no chart at all, suggesting that something is obviously amiss. Do I need to make additional changes to the Javascript in order to tell it about the object? My assumption is no, that I'm passing this information view the context_data.
I'm also using Ajax, so I'm not sure if that is a complicating factor. Here is my javascript.
<script>
var endpoint = '{% url "myobject:chart_data" %}'
var defaultData = [];
var labels = [];
$.ajax({
method: "GET",
credentials: 'same-origin',
url: endpoint,
success: function(data){
labels = data.labels
defaultData = data.default
var ctx = document.getElementById("myChart");
var myChart = new Chart(ctx, {
type: 'pie',
data: {
labels: [{% for i in myobject %}{{ i.labels }},{% endfor %}],
datasets: [{
data: [{% for i in myobject %}{{ i.data }},{% endfor %}]
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(153, 102, 255, 0.2)',
],
borderColor: [
'rgba(255,99,132,1)',
'rgba(153, 102, 255, 1)',
],
borderWidth: 1
}]
}
})
},
error: function(error_data){
console.log("error")
console.log(error_data)
}
})
</script>
You don't necessarily need to use serializers to render dynamic data in Chart.js (although you can if you would like). You can just iterate through your Django context variables like normal in the appropriate locations. Below is a line chart example. I would play around with this example, but this should show you how to easily render dynamic data with Django context variables.
...
<canvas id="funchart" width="75" height="50"></canvas>
<script type="text/javascript">
var a = document.getElementById('funchart').getContext('2d');
var myLineChart = new Chart(a, {
type: 'line',
data: {
labels:[{% for i in myobject %}{{ i.labels }},{% endfor %}],
datasets: [{
label:'My Dot',
data: [{% for i in myobject %}{{ i.data }},{% endfor %}]
}]
},
options:{
scales: {
xAxes: [{
display:true
}],
yAxes: [{
ticks: {
beginAtZero:true
}
}]
}
}
});
</script>
so i have been working the same stuff here, i think your label not showing up because the label on chart.js only wants a string, so try this on your labels:
labels: [{% for i in book %}"{{ i.id }}",{% endfor %}]
My code below is what ultimately solved the problem on how to get dynamic user data. I am still trying to figure out how to get the labels to work properly. This solution allows the labels to display with ID numbers, which isn't optimal, but I opened a separate SO for the label issue.
My HTML
{% extends 'base5.html' %}
{% block body_block %}
<div class="box6">
<h1 class="title">Number of Books By Author</h1>
<canvas id="myChart"></canvas>
<div>
<script>
var endpoint = '{% url "Books:chart_data" %}'
var defaultData = [];
var labels = [];
array = {{ procedures3 }}
$.ajax({
method: "GET",
credentials: 'same-origin',
url: endpoint,
success: function(data){
defaultData = data.default
var ctx = document.getElementById("myChart");
var myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: [{% for i in book %}{{ i.id }},{% endfor %}],
datasets: [{
label: "# of Procedures",
data: [{% for j in book_count %}
{{ j }},
{% endfor %}],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
],
borderColor: [
'rgba(255,99,132,1)',
'rgba(255,99,132,1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
}
})
},
error: function(error_data){
console.log("error")
console.log(error_data)
},
})
</script>
{% endblock %}
Views.py
ChartView
class ChartData(LoginRequiredMixin,APIView):
model = Author
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated,)
def get(self, request, format=None):
default_items = []
labels = []
data = {
"labels": labels,
"default": default_items,
}
return Response(data)
The ChartView
class ChartView(LoginRequiredMixin, TemplateView): template_name = 'Book/chart.html'
def get_context_data(self, **kwargs):
context = super(ChartView, self).get_context_data(**kwargs)
book = Author.objects.filter(id__in=self.request.user.userprofile.author.all()).order_by('id')
books_count = [ Book.objects.filter(author=cls).count() for cls in book ]
context['book'] = book
context['book_count'] = books_count
return context
Related
I know this question has been asked a fair bit on here, but the solution that's been going around hasn't been working for me.
My chart renders in real time based on the start/end dates provided on the front-end (HTML date input widget). Since the default start time for that widget is 01/01/2019, charts.js will create a chart for the 01/01/2019 start/end times, and then proceed to produce the chart for the specified start/end dates. The 01/01/2019 chart shows on hover and I'd like to get rid of it.
function dayChart(labels_day, data_day) {
var ctx_day = document.getElementById('day-chart').getContext('2d');
var config = {
type: 'bar',
data: {
labels: labels_day,
// labels: ['Early Morning (00:00-05:59)', 'Morning (06:00-11:59)', 'Afternoon (12:00-18:59)', 'Night (19:00-23:59)'],
datasets: [{
label: '# of Orders by Day of the Week',
data: data_day,
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)',
'rgba(255, 99, 132, 0.2)',
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)',
'rgba(255, 99, 132, 1)',
],
borderWidth: 1
}]
},
options: {
responsive: true,
title: {
display: true,
text: 'Taxi Orders calculated by day of the week'
},
scales: {
yAxes: [{
id: data_day,
display: true,
ticks: {
stepsize: 500,
min: 0,
}
}]
},
}
}
var chart_day = new Chart (ctx_day, config)
if (chart_day) {
chart_day.destroy();
chart_day = new Chart (ctx_day, config);
chart_day.update();
}
}
form section of html file
<form name="date-input" method="POST" action="/date-input/">
<label for="start">Start date:</label>
<input type="date" id="start" name="start_date" value="2019-01-01" min="2019-01-01" max="2019-06-30">
<label for="end">End date:</label>
<input type="date" id="end" name="end_date" value="2019-01-01" min="2019-01-01" max="2019-07-01">
</form>
async call for start/end dates
<script src="https://code.jquery.com/jquery-3.5.1.js"></script>
<script>
// global variables - so that chartjs doesn't show old data on hover
var chart_day;
var chart_hour;
var chart_payment;
var start_date = Date.parse($("#start").val()) / 1000; // const can't be re-assigned or redeclared. Date() to convert datetime into epoch time
var end_date = Date.parse($("#end").val()) / 1000; // divide by 1000 to get 10 digits (compatible with values in DB)
function ajaxRequest() {
var start_date = Date.parse($("#start").val()) / 1000;
var end_date = Date.parse($("#end").val()) / 1000;
console.log(start_date)
console.log(end_date)
$.ajax({ // initialize an AJAX request
type: "POST",
url: '/get-data/', // calls data from /date-input/ endpoint
data: {
'start_date': start_date, // add the order id to the POST parameters
'end_date': end_date,
'csrfmiddlewaretoken': "{{csrf_token}}",
},
success: function (data) { // `data` is from `homepage` view function
labels_payment = data.labels_payment
data_payment = data.data_payment
labels_hour = data.labels_hour
data_hour = data.data_hour
labels_day = data.labels_day
data_day = data.data_day
// call payment, hour, and dow methods to render in the frontend
setPaymentMethodChart(labels_payment, data_payment)
hourlyChart(labels_hour, data_hour)
dayChart(labels_day, data_day)
// console.log(data)
console.log(data.start_date)
console.log(data.end_date)
console.log(data.labels_payment)
console.log(data.data_payment)
},
error: function (data, xhr, status, error) {
console.log("yikes")
}
});
}
You correctly defined chart_day globally and even commented it as follows:
global variables - so that chartjs doesn't show old data on hover
The problem however lies in your dayChart function. Instead of using the globally defined chart_day variable, you defined an identically named variable in the local scope of the function.
The solution would be to replace the following code block:
var chart_day = new Chart (ctx_day, config)
if (chart_day) {
chart_day.destroy();
chart_day = new Chart (ctx_day, config);
chart_day.update();
}
...with this one:
if (chart_day) {
chart_day.destroy();
}
chart_day = new Chart (ctx_day, config);
I'm trying to do a selection on what only will be displayed on my chart, but I don't know what it's called hehe. please help
my views.py look like this.
class ResView(LoginRequiredMixin, ListView):
model = Rainfall
template_name = 'home.html'
context_object_name = 'res'
paginate_by = 8
queryset = Rainfall.objects.all().order_by('-id')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["qs"] = Rainfall.objects.all()
return context
my chart look like this.
<script>
(function () {
var ctx = document.getElementById('myChart').getContext('2d');
var myChart = new Chart(ctx, {
type: 'line',
data: {
labels: [{% for i in qs %}'{{i.timestamp}}',{% endfor %}],
datasets: [{
label: 'Rainfall Graph',
data: [{% for i in qs %}'{{i.amount}}',{% endfor %}],
lineTension: 0,
backgroundColor: 'transparent',
borderColor: '#c9c5c5',
borderWidth: 2,
pointRadius: 1,
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: false
}
}]
},
legend: {
display: false
}
}
})
}());
Now I want to select what to display on my chart. something like a dropdown like this. That I have an option.
<select>
<option value="hour">Hour</option>
<option value="day">Day</option>
<option value="month">Month</option>
</select>
I want to count the number of times a choice is selected by users. Here is my model
models.py
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
from multiselectfield import MultiSelectField
class MedicalHistory(models.Model):
Anxiety = 'Anxiety'
Arthritis = 'Arthritis'
Asthma = 'Asthma'
Anemia = 'Anemia'
Cancer = 'Cancer'
Corona_virus = 'Corona_virus'
Diabetes = 'Diabetes'
Ebola = 'Ebola'
HIV = 'HIV'
ILLNESS_CHOICES = (
(Anxiety, "Anxiety"),
(Arthritis, "Arthritis"),
(Asthma, "Asthma"),
(Anemia, "Anemia"),
(Cancer, "Cancer"),
(Corona_virus, "Corona_virus"),
(Diabetes, "Diabetes"),
(Ebola, "Ebola"),
(HIV, "HIV"),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
illness = MultiSelectField(choices=ILLNESS_CHOICES, max_length=50)
symptoms = models.CharField(max_length=100)
additional_info = models.CharField(max_length=100)
disability = models.BooleanField(default=False)
medications = models.BooleanField(default=False)
created_at = models.DateTimeField(default=timezone.now)
def __str__(self):
return f'{self.user.username} Medical History'
Here I have a number of illnesses I want users to select from. A user can select more tan one illness. I want each illness to have a count, and every time the illness is selected it adds to the count. In my view I have
views.py
def pie_chart(request):
labels = []
data = []
queryset = MedicalHistory.objects.values('illness').annotate(count=Sum('user')).order_by('-count')
for entry in queryset:
labels.append(entry['illness'])
data.append(entry['count'])
return JsonResponse(data={
'labels': labels,
'data': data,
})
<QuerySet [{'illness': ['Asthma', 'Diabetes', 'Ebola'], 'count': 3}, {'illness': ['Anemia', 'Covid-19'], 'count': 2}]>
The query doesn't do what I want, as I am trying to plot it in a chart. It groups the illness I think because of the values. My template.html looks like this.
chart.html
{% block content %}
{% include 'sidebar.html' %}
<div id="container" style="width: 85%;">
<canvas id="myChart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js#2.9.3/dist/Chart.min.js"></script>
<script>
var ctx = document.getElementById('myChart');
var myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: {{ labels|safe }},
datasets: [{
label: 'Overview of Medical History',
data: {{ data|safe }},
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(225, 400, 64, 0.2)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
}
}
});
</script>
</div>
<!-- /#page-content-wrapper -->
</div>
<!-- /#wrapper -->
{% endblock %}
Solution:
So I was able to solve this using python. I initialized a dict with the illnesses and setting their initial count as 0. Then I used values() returns a dictionary of illnesses saved in the db. The I looped through the queryset and concatenate 1 to the values of the illnesses. Then i passed the keys and values as labels and data respectively.
views.py
def pie_chart(request):
count = {'Anxiety': 0, 'Arthritis': 0, 'Asthma': 0, 'Anemia': 0, 'Cancer': 0,
'Corona_virus': 0, 'Diabetes': 0, 'Ebola': 0, 'HIV': 0
}
queryset = MedicalHistory.objects.values('illness')
for entry in queryset:
for values in entry['illness']:
count[values] += 1
labels = [*count.keys()]
data = [*count.values()]
return render(request, 'chart.html', {
'labels': labels,
'data': data,
})
You can't do that at the database level if you're using a MultiSelectField because the underlying value is just stored as CharField in the database. So values('illness') will only group by equal strings, i.e. combinations of illnesses.
So either do this in python, after you've fetched all the instances: Initialize a dict of counters, loop through the instances in your queryset and update the counters one by one. Note: This won't scale when the number of MedicalHistory rows in your table becomes large.
Or model your Illness as a separate model (don't use MultiSelectField), use a ManyToMany relationship and count those. Then you can do this in a queryset. This will scale with a very large number of rows.
For some data visualisation I use ractive and chart.js. The initial drawing works great, but I can't find a way to update the chart automatically when my data changes. So far I got (simplified):
const Stats = '<canvas id="myChart" width="400" height="400"></canvas>'
new Ractive ({
el: '#stats',
template: Stats,
magic: true,
modifyArrays: true,
data: {docs}, // <= some JSON Data
computed: {
Data1() {
let tempList = this.get('docs');
// rearrange & filter Data
return tempList ;
},
Data2() {
let tempList2 = this.get('docs');
// rearrange & filter Data
return tempList2 ;
},
Data3() {
let tempList3 = this.get('docs');
// rearrange & filter Data
return tempList3 ;
},
}
},
onrender: function () {
let DataSet1 = this.get('Data1');
let DataSet2 = this.get('Data2');
let DataSet3 = this.get('Data3');
let ctx = document.getElementById("myChart");
var myChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ["Data 1", "Data 1", "Data 3"],
datasets: [{
label: 'All my Data',
data: [DataSet1.length, DataSet2.length, DataSet3.length],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(255, 159, 64, 0.2)',
'rgba(75, 192, 192, 0.2)'
],
borderColor: [
'rgba(255,99,132,1)',
'rgba(255, 159, 64, 1)',
'rgba(75, 192, 192, 1)'
],
borderWidth: 1
}]
},
options: {
responsive: false
}
});
},
onchange: function () {
let newData = this.get('docs')
addData(myChart, label, newData)
function addData(chart, label, data) {
chart.data.labels.push(label);
chart.data.datasets.forEach((dataset) => {
dataset.data.push(data);
});
chart.update();
}
}
});
Of Course I get an error in line chart.data.labels.push(label);, and also I'm quite sure that I would need my computed values in the onrenderfunction, not the initial Dataset. But I really have no clue how to get those into the update function, or if this function is the correct approach at all...
If you are working with 3rd party plugins I would recommend to use Ractive's decorators.
This post on ractivejs-and-jquery-plugins shows you a starting point how to implement a decorator based on a fileupload control.
In your case I would recommend to build a decorator with the data as parameter and do not forget to implement the UPDATE function (where in your case you gonna call the update method with the new data for your chart)
As I could not figure out how to write my own decorator for Chart.js I thought I post the solution that worked for me. I do not think it's best practice and for sure decorators would be a better way (therefore I do not mark this solution as correct answer), but it works:
const Stats = '<canvas id="myChart" width="400" height="400"></canvas>'
new Ractive ({
el: '#stats',
template: Stats,
magic: true,
modifyArrays: true,
data: {docs}, // <= some JSON Data
computed: {
Data1() {
let tempList = this.get('docs');
// rearrange & filter Data
return tempList ;
},
Data2() {
let tempList2 = this.get('docs');
// rearrange & filter Data
return tempList2 ;
},
Data3() {
let tempList3 = this.get('docs');
// rearrange & filter Data
return tempList3 ;
},
}
},
onrender: function () {
let DataSet1 = this.get('Data1');
let DataSet2 = this.get('Data2');
let DataSet3 = this.get('Data3');
let ctx = document.getElementById("myChart");
myChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ["Data 1", "Data 1", "Data 3"],
datasets: [{
label: 'All my Data',
data: [DataSet1.length, DataSet2.length, DataSet3.length]
}]
}
});
},
onchange: function () {
let changedData1 = this.get('Data1');
let changedData2 = this.get('Data2');
let changedData3 = this.get('Data3');
myChart.data.datasets[0].data[0] = changedData1.length;
myChart.data.datasets[0].data[1] = changedData2.length;
myChart.data.datasets[0].data[2] = changedData3.length;
myChart.update();
}
}
});
Using chart.js v2, is it possible to mark a dataset in a line chart as being disabled on initial load?
Didn't find an option for it in the documentation.
Yes, there is a "hidden" flag in ChartJS.
eg.
data:
{
datasets: [
{
data: [1,2,3],
label: 'My First Dataset',
hidden: true,
},
],
}
See this issue on GitHub: https://github.com/chartjs/Chart.js/issues/689
The accepted solution has the downside, that hiding/unhiding signals might sometimes fail after initializing the chart like that.
It might be a better idea to change it in the current metadata of the dataSet, which holds the actual data that is used by the chart:
chart.data.datasets.forEach((dataSet, i) => {
var meta = chart.getDatasetMeta(i);
meta.hidden = (<your-condition-here>);
});
this.chart.update();
If you are using angular-chartjs, then you can add the properties of the dataset in the chart-dataset-override property:
For example:
HTML:
<div class="container" ng-app="app" ng-controller="ChartCtrl">
<canvas id="bar" class="chart chart-bar" chart-data="data" chart-labels="labels" chart-series="series" chart-dataset-override="datasetOverride">
</canvas>
</div>
Javascript:
Chart.defaults.global.legend.display = true;
angular.module("app", ["chart.js"])
.controller("ChartCtrl", function($scope) {
$scope.labels = ['2006', '2007', '2008', '2009', '2010', '2011', '2012'];
$scope.series = ['Series A', 'Series B'];
$scope.data = [
[65, 59, 80, 81, 56, 55, 40],
[28, 48, 40, 19, 86, 27, 90]
];
$scope.datasetOverride = [{}, {
hidden: true,
}];
});