created MultipleChartsView
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# Requirements
|
||||
|
||||
django
|
||||
django-bootstrap3
|
||||
django-select2
|
||||
django-watson
|
||||
|
||||
@@ -1,61 +1,88 @@
|
||||
from django.db.models import (Field, CharField, NullBooleanField,
|
||||
from django.db.models import (Field, NullBooleanField,
|
||||
Count,
|
||||
)
|
||||
from graphos.sources.simple import SimpleDataSource
|
||||
from graphos.renderers import gchart
|
||||
|
||||
# Defines generic labels for common fields
|
||||
LABELS = {
|
||||
NullBooleanField: {True: "Oui", False: "Non", None:"Ne sait pas"},
|
||||
}
|
||||
|
||||
class FieldValuesCountDataSource(SimpleDataSource):
|
||||
""" Generates data from a limited set of choices.
|
||||
|
||||
"""
|
||||
def __init__(self, queryset, field, labels=None, excluded=[]):
|
||||
self.queryset = queryset
|
||||
self.field_name = field.name
|
||||
self.excluded = excluded
|
||||
if not labels:
|
||||
if field.__class__ in LABELS:
|
||||
labels = LABELS[field.__class__]
|
||||
elif field.choices:
|
||||
labels = dict(field.choices)
|
||||
else:
|
||||
raise ValueError("Could not retrieve labels for", field)
|
||||
self.labels = labels
|
||||
super().__init__(self.create_data())
|
||||
|
||||
def create_data(self):
|
||||
data = [(self.field_name, "%s_count" % self.field_name)] # Headers
|
||||
data += [
|
||||
(self.labels[item[self.field_name]], # Display a label instead of raw values
|
||||
item['count']
|
||||
) for item in self.queryset.values( # Retrieve all values for field
|
||||
self.field_name
|
||||
).annotate( # Count occurrences of each value
|
||||
count=Count('pk')
|
||||
).order_by() # Needed so that counts are aggregated
|
||||
# Exclude values that are marked to be ignored
|
||||
if (not self.excluded
|
||||
or item[self.field_name] not in self.excluded)
|
||||
]
|
||||
return data
|
||||
|
||||
|
||||
class PieWrapper(gchart.PieChart):
|
||||
""" A wrapper around gchart.PieChart that generates a graph from :
|
||||
|
||||
- a queryset and a model field (NullBooleanField or field with choices)
|
||||
OR
|
||||
- a data object and title
|
||||
"""
|
||||
|
||||
height=400
|
||||
width=800
|
||||
labels = {
|
||||
NullBooleanField: {True: "Oui", False: "Non", None:"Ne sait pas"},
|
||||
}
|
||||
|
||||
def __init__( self, queryset=None, field=None,
|
||||
def __init__(self,
|
||||
queryset=None, field=None,
|
||||
data=None, title=None,
|
||||
null_values=[],**kwargs):
|
||||
null_values=[],
|
||||
**kwargs):
|
||||
if not data:
|
||||
if not isinstance(field, Field):
|
||||
raise TypeError(field, 'must be a child of django.models.db.fields.Field !')
|
||||
if not queryset:
|
||||
raise TypeError("You must give either a queryset and field or data")
|
||||
|
||||
if field.__class__ in PieWrapper.labels:
|
||||
labels = PieWrapper.labels[field.__class__]
|
||||
elif field.choices:
|
||||
labels = dict(field.choices)
|
||||
data_source = FieldValuesCountDataSource(
|
||||
queryset, field,
|
||||
excluded=null_values,
|
||||
labels=None #TODO: How to pass in labels ??
|
||||
)
|
||||
else:
|
||||
raise ValueError("Could not guess labels for", field)
|
||||
|
||||
data = ([(field.name, 'count')] + # Headers
|
||||
[(labels[item[field.name]],
|
||||
item['nbr']) for item in queryset.values(
|
||||
field.name
|
||||
).annotate(
|
||||
nbr=Count('pk')
|
||||
).order_by()
|
||||
if (not null_values
|
||||
or item[field] not in null_values)
|
||||
])
|
||||
data_source = SimpleDataSource(data=data)
|
||||
|
||||
super().__init__(
|
||||
SimpleDataSource(
|
||||
data=data
|
||||
),
|
||||
data_source,
|
||||
options={
|
||||
'title': getattr(field, 'verbose_name', title),
|
||||
'is3D': True,
|
||||
'pieSliceText': 'value',
|
||||
'legend': {'position': 'labeled', 'maxLines': 3, 'textStyle': {'fontSize': 16,}},
|
||||
},
|
||||
width=kwargs.get('width', self.width), height=kwargs.get('height', self.height),
|
||||
width=kwargs.get('width', self.width),
|
||||
height=kwargs.get('height', self.height),
|
||||
)
|
||||
|
||||
def get_js_template(self):
|
||||
|
||||
@@ -1,5 +1,2 @@
|
||||
<div class="row text-center" id="wrapper-{{ chart.get_html_id}}">
|
||||
<div id="{{ chart.get_html_id }}" style="width: {{ chart.width }}px; height: {{ chart.height }}px;"></div>
|
||||
<a class="btn btn-sm btn-default" href="#" id="image-{{ chart.get_html_id }}"><span class="glyphicon glyphicon-save-file"></span> Télécharger l'image</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" {% active namespace="statistiques" viewname="pies" %}>
|
||||
<a href="{% url "statistiques:pies" %}?year={{year|default:0}}{% if month %}&month={{month}}{% endif %}">Typologie du public
|
||||
<span class="glyphicon glyphicon-adjust"></span>
|
||||
<a href="{% url "statistiques:pies" %}?year={{year|default:0}}{% if month %}&month={{month}}{% endif %}">Typologie
|
||||
<span class="glyphicon glyphicon-user"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" {% active namespace="statistiques" viewname="frequentation" %}>
|
||||
<a href="{% url "statistiques:frequentation" %}?year={{year|default:0}}{% if month %}&month={{month}}{% endif %}">Fréquentation
|
||||
<span class="glyphicon glyphicon-adjust"></span>
|
||||
<span class="glyphicon glyphicon-stats"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
43
statistiques/templates/statistiques/multiple_charts.html
Normal file
43
statistiques/templates/statistiques/multiple_charts.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% extends "statistiques/base.html" %}
|
||||
|
||||
{% block title %}{{block.super}} {{page_title}}{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}{{block.super}}<li>{{page_title}}</li>{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{{ block.super }}
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-body text-right">
|
||||
{% include "statistiques/filter_form.html" %}
|
||||
<hr />
|
||||
<p>Échantillon : {{ queryset_count }} objets</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<script type="text/javascript">
|
||||
function hideAll() {
|
||||
{% for _, graph in graphs %}{% with graph.get_html_id as id %}
|
||||
$("#tab-{{id}}").attr("class", "");
|
||||
$("#wrapper-{{id}}").hide();
|
||||
{% endwith %}{% endfor %}
|
||||
}
|
||||
|
||||
function showGraph(id) {
|
||||
hideAll();
|
||||
$("#tab-" + id).attr("class", "active");
|
||||
$("#wrapper-" + id).show();
|
||||
}
|
||||
</script>
|
||||
<ul class="nav nav-tabs">
|
||||
{% for title, graph in graphs %}<li role="presentation" id="tab-{{graph.get_html_id}}"><a href="#" onclick="showGraph('{{graph.get_html_id}}');">{{ title }}</a></li>{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% for title, graph in graphs %}
|
||||
<div class="row text-center" id="wrapper-{{ graph.get_html_id}}">
|
||||
{{ graph.as_html }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,9 +1,8 @@
|
||||
import datetime
|
||||
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import redirect
|
||||
from django.views import generic
|
||||
from django.db.models import (Field, CharField, NullBooleanField,
|
||||
from django.db.models import (CharField, NullBooleanField,
|
||||
Count,
|
||||
)
|
||||
from django.db.models.functions.datetime import ExtractMonth
|
||||
@@ -20,8 +19,8 @@ from notes.models import Sujet
|
||||
|
||||
###
|
||||
|
||||
|
||||
nom_mois = {
|
||||
NO_DATA = "Aucune donnée"
|
||||
NOM_MOIS = {
|
||||
1: "Janvier",
|
||||
2: "Février",
|
||||
3: "Mars",
|
||||
@@ -40,13 +39,22 @@ nom_mois = {
|
||||
class FilterMixin(generic.edit.FormMixin):
|
||||
|
||||
form_class = SelectRangeForm
|
||||
request = None
|
||||
year = None
|
||||
month = None
|
||||
|
||||
def get_initial(self):
|
||||
return {'month': self.request.GET.get('month', 0), 'year': self.request.GET.get('year', 0) }
|
||||
return {
|
||||
'month': self.request.GET.get('month', 0),
|
||||
'year': self.request.GET.get('year', 0),
|
||||
}
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
self.year = int(self.request.GET.get('year', 0))
|
||||
self.month = int(self.request.GET.get('month', 0))
|
||||
def parse_args(self, request):
|
||||
self.year = int(request.GET.get('year', 0))
|
||||
self.month = int(request.GET.get('month', 0))
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.parse_args(request)
|
||||
return super().get(self, *args, **kwargs)
|
||||
|
||||
def _filters(self, prefix):
|
||||
@@ -74,7 +82,27 @@ class FilterMixin(generic.edit.FormMixin):
|
||||
return context
|
||||
|
||||
|
||||
NO_DATA = "Aucune donnée"
|
||||
class MultipleChartsView(FilterMixin, generic.TemplateView):
|
||||
|
||||
template_name = "statistiques/multiple_charts.html"
|
||||
page_title = None
|
||||
|
||||
def get_queryset(self):
|
||||
raise NotImplementedError("Subclass must implement this method")
|
||||
|
||||
def get_graphs(self, queryset):
|
||||
raise NotImplementedError("Subclasses must implement this method.")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
queryset = self.get_queryset()
|
||||
context['page_title'] = str(self.page_title)
|
||||
context['queryset_count'] = queryset.count()
|
||||
context['graphs'] = [
|
||||
(title, graph) for title, graph in self.get_graphs(queryset)
|
||||
]
|
||||
return context
|
||||
|
||||
|
||||
class DashboardView(FilterMixin, generic.TemplateView):
|
||||
template_name = "statistiques/index.html"
|
||||
@@ -120,11 +148,13 @@ class DashboardView(FilterMixin, generic.TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
class PieChartView(MultipleChartsView):
|
||||
page_title = "Typologie"
|
||||
|
||||
class PieChartView(FilterMixin, generic.TemplateView):
|
||||
template_name = "statistiques/typologie.html"
|
||||
def get_queryset(self):
|
||||
return self.get_fichestatistiques_queryset()
|
||||
|
||||
def get_graphs(self):
|
||||
def get_graphs(self, queryset):
|
||||
sujets = self.get_sujets_queryset()
|
||||
# Insertion des champs 'âge' et 'genre' du modèle notes.Sujet
|
||||
for field in Sujet._meta.fields:
|
||||
@@ -150,22 +180,13 @@ class PieChartView(FilterMixin, generic.TemplateView):
|
||||
|
||||
# Puis des champs du modèle statistiques.FicheStatistique
|
||||
# dans leur ordre de déclaration
|
||||
queryset = self.get_fichestatistiques_queryset()
|
||||
for field in FicheStatistique._meta.fields:
|
||||
if field.__class__ in (NullBooleanField, CharField):
|
||||
yield str(field.verbose_name), PieWrapper(queryset, field)
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['graphs'] = [(title, graph) for title, graph in self.get_graphs()]
|
||||
context['queryset'] = self.get_fichestatistiques_queryset()
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class FrequentationStatsView(FilterMixin, generic.TemplateView):
|
||||
template_name = "statistiques/frequentation.html"
|
||||
class FrequentationStatsView(MultipleChartsView):
|
||||
page_title = "Fréquentation"
|
||||
|
||||
@staticmethod
|
||||
def calculer_frequentation_par_quart_heure(observations, continu=False):
|
||||
@@ -207,7 +228,6 @@ class FrequentationStatsView(FilterMixin, generic.TemplateView):
|
||||
and (debut.hour <= heure and debut.minute <= debut_intervalle)
|
||||
and (fin.hour >= heure and fin.minute >= fin_intervalle)):
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -224,16 +244,15 @@ class FrequentationStatsView(FilterMixin, generic.TemplateView):
|
||||
|
||||
return data
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_observations_queryset()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
observations = self.get_observations_queryset()
|
||||
|
||||
par_heure = self.calculer_frequentation_par_quart_heure(observations, continu=False)
|
||||
en_continu = self.calculer_frequentation_par_quart_heure(observations, continu=True)
|
||||
|
||||
context['rencontres_par_heure'] = gchart.AreaChart(
|
||||
def get_graphs(self, queryset):
|
||||
graphs = []
|
||||
# Nombre de rencontres en fonction de l'heure
|
||||
par_heure = self.calculer_frequentation_par_quart_heure(queryset, continu=False)
|
||||
en_continu = self.calculer_frequentation_par_quart_heure(queryset, continu=True)
|
||||
graphs.append(("Par heure", gchart.AreaChart(
|
||||
SimpleDataSource(
|
||||
[("Heure", "Rencontres démarrées", "Au total (démarré + en cours)")] +
|
||||
[(heure, par_heure[heure], en_continu[heure]) for heure in sorted(par_heure.keys())]
|
||||
@@ -241,29 +260,28 @@ class FrequentationStatsView(FilterMixin, generic.TemplateView):
|
||||
options={
|
||||
"title": "Fréquentation de la maraude en fonction de l'heure (par quart d'heure)"
|
||||
}
|
||||
)
|
||||
)))
|
||||
|
||||
par_mois = observations.order_by().annotate(
|
||||
# Nombre de rencontres en fonction du mois
|
||||
par_mois = queryset.annotate(
|
||||
mois=ExtractMonth('created_date')
|
||||
).values(
|
||||
'mois'
|
||||
).annotate(
|
||||
nbr=Count('pk')
|
||||
)
|
||||
context['rencontres_par_mois'] = ColumnWrapper(
|
||||
).order_by()
|
||||
graphs.append(("Par mois", ColumnWrapper(
|
||||
SimpleDataSource(
|
||||
[("Mois", "Rencontres")] +
|
||||
[(nom_mois[item['mois']], item['nbr']) for item in par_mois]
|
||||
[(NOM_MOIS[item['mois']], item['nbr']) for item in par_mois]
|
||||
),
|
||||
options={
|
||||
"title": "Nombre de rencontres par mois"
|
||||
}
|
||||
)
|
||||
)))
|
||||
|
||||
# Graph: Fréquence de rencontres par sujet
|
||||
nbr_rencontres = observations.values('sujet').annotate(nbr=Count('pk')).order_by()
|
||||
context['rencontres_par_sujet'] = nbr_rencontres.count()
|
||||
|
||||
nbr_rencontres = queryset.values('sujet').annotate(nbr=Count('pk')).order_by()
|
||||
categories = (
|
||||
('Rencontre unique', (1,)),
|
||||
('Entre 2 et 5 rencontres', range(2, 6)),
|
||||
@@ -271,17 +289,19 @@ class FrequentationStatsView(FilterMixin, generic.TemplateView):
|
||||
('Plus de 20 rencontres', range(20, 999)),
|
||||
)
|
||||
get_count_for_range = lambda rg: nbr_rencontres.filter(nbr__in=rg).count()
|
||||
context['rencontres_par_sujet'] = PieWrapper(
|
||||
graphs.append(("Par sujet", PieWrapper(
|
||||
data=[('Type de rencontre', 'Nombre de sujets')] +
|
||||
[(label, get_count_for_range(rg)) for label, rg in categories],
|
||||
title='Fréquence de rencontres'
|
||||
)
|
||||
)))
|
||||
|
||||
return context
|
||||
# Rencontres par lieu
|
||||
# TODO: More customizable way of categorizing "Lieu"
|
||||
|
||||
return graphs
|
||||
|
||||
|
||||
# AjaxMixin
|
||||
|
||||
class AjaxOrRedirectMixin:
|
||||
""" For view that should be retrieved by Ajax only. If not,
|
||||
redirects to the primary view where these are displayed """
|
||||
@@ -293,14 +313,12 @@ class AjaxOrRedirectMixin:
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
class StatistiquesDetailsView(AjaxOrRedirectMixin, generic.DetailView):
|
||||
|
||||
model = FicheStatistique
|
||||
template_name = "statistiques/fiche_stats_details.html"
|
||||
|
||||
|
||||
|
||||
class StatistiquesUpdateView(AjaxOrRedirectMixin, generic.UpdateView):
|
||||
|
||||
model = FicheStatistique
|
||||
|
||||
Reference in New Issue
Block a user