Files
django-maraudes/statistiques/charts.py

277 lines
9.7 KiB
Python

import datetime
import collections
from django.db.models.functions.datetime import ExtractMonth
from django.db.models import (Field, NullBooleanField,
Count,
)
from graphos.sources.simple import SimpleDataSource
from graphos.renderers import gchart
from notes.models import Sujet
NOM_MOIS = {
1: "Janvier",
2: "Février",
3: "Mars",
4: "Avril",
5: "Mai",
6: "Juin",
7: "Juillet",
8: "Août",
9: "Septembre",
10: "Octobre",
11: "Novembre",
12: "Décembre"
}
# Defines generic labels for common fields
LABELS = {
NullBooleanField: {True: "Oui", False: "Non", None: "Ne sait pas"},
}
# TODO: Retrieve charts data from cache to avoid recalculating on each request
class CachedDataSource:
pass
# TODO: Clean up...
class FieldValuesCountDataSource(SimpleDataSource):
""" Generates data from a limited set of choices.
"""
def __init__(self, queryset, field, labels=None, excluded=None):
self.queryset = queryset
self.field_name = field.name
self.excluded = excluded or []
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
"""
options = {
'is3D': False,
'pieSliceText': 'value',
'legend': {'position': 'labeled', 'maxLines': 3, 'textStyle': {'fontSize': 16, }},
}
height = 400
width = 800
def __init__(self,
queryset=None, field=None,
data=None, title=None,
null_values=None,
**kwargs):
if data is None:
if not isinstance(field, Field):
raise TypeError(field, 'must be a child of django.models.db.fields.Field !')
data_source = FieldValuesCountDataSource(
queryset, field,
excluded=null_values,
labels=None # TODO: How to pass in labels ??
)
else:
data_source = SimpleDataSource(data=data)
options = self.options.copy()
options.update(
title=getattr(field, 'verbose_name', title)
)
super().__init__(data_source,
options=options,
width=kwargs.get('width', self.width),
height=kwargs.get('height', self.height),
)
def get_js_template(self):
return "statistiques/gchart/pie_chart.html"
def get_html_template(self):
return "statistiques/gchart/html.html"
class ColumnWrapper(gchart.ColumnChart):
options = {
'is3D': False,
'legend': {'position': 'labeled', 'maxLines': 3, 'textStyle': {'fontSize': 16, }},
'title': 'test',
}
def __init__(self, *args, **kwargs):
kwargs.update(options=self.options.copy())
super().__init__(*args, **kwargs)
class DonneeGeneraleChart(gchart.BarChart):
def __init__(self, maraudes=None, rencontres=None, sujets=None):
data = [("...", "Soirée", "Journée", {'role': 'annotation'})]
data += [("Maraudes", maraudes[2].count(), maraudes[1].count(), maraudes[0].count())]
data += [("Rencontres", rencontres[2].count(), rencontres[1].count(), rencontres[0].count())]
if sujets:
data += [("Nouvelles rencontres", sujets[2].count(), sujets[1].count(), sujets[0].count())]
super().__init__(SimpleDataSource(data), options={'title': 'Données générales', 'isStacked': 'percent'})
def generate_piechart_for_field(field):
""" Returns a PieWrapper subclass working with a fixed field """
class FieldChart(PieWrapper):
def __init__(self, queryset):
super().__init__(queryset, field)
return FieldChart
class GenrePieChart(PieWrapper):
def __init__(self, queryset):
queryset = Sujet.objects.filter(pk__in=queryset.values_list('pk'))
super().__init__(queryset, Sujet._meta.get_field('genre'))
class AgePieChart(PieWrapper):
""" Chart for 'age' field of Sujet model """
labels = (('Mineurs', range(0, 18)),
('18-24', range(18, 25)),
('25-34', range(25, 35)),
('35-44', range(35, 45)),
('45-54', range(45, 55)),
('+ de 55', range(55, 110)),
)
def count_nbr_sujets(self, age_rng):
return self.queryset.filter(age__in=age_rng).count()
def __init__(self, queryset):
data = [("age", "count")]
if queryset:
self.queryset = Sujet.objects.filter(pk__in=queryset.values_list('pk'))
data += [(label, self.count_nbr_sujets(rg)) for label, rg in self.labels]
data += [("Ne sait pas", self.queryset.filter(age=None).count())]
super().__init__(data=data, title="Âge")
class RencontreParSujetChart(PieWrapper):
labels = (('Rencontre unique', (1,)),
('Entre 2 et 5 rencontres', range(2, 6)),
('Entre 6 et 20 rencontres', range(6, 20)),
('Plus de 20 rencontres', range(20, 999)),
)
def get_count_for_range(self, rng):
return self.queryset.filter(nbr__in=rng).count()
def __init__(self, queryset):
data = [('Type de rencontre', 'Nombre de sujets')]
if queryset:
self.queryset = queryset.values('sujet').annotate(nbr=Count('pk')).order_by()
data += [(label, self.get_count_for_range(rg)) for label, rg in self.labels]
super().__init__(data=data,
title='Fréquence de rencontres'
)
class RencontreParMoisChart(gchart.ColumnChart):
def __init__(self, queryset):
data = [("Mois", "Rencontres")]
if queryset:
par_mois = queryset.annotate(
mois=ExtractMonth('created_date')
).values(
'mois'
).annotate(
nbr=Count('pk')
).order_by()
data += [(NOM_MOIS[item['mois']], item['nbr']) for item in par_mois]
else:
data += [("Mois", 0)]
super().__init__(SimpleDataSource(data),
options={
"title": "Nombre de rencontres par mois"
}
)
class RencontreParHeureChart(gchart.AreaChart):
def __init__(self, queryset):
data = [("Heure", "Rencontres démarrées", "Au total (démarré + en cours)")]
if queryset:
par_heure = self.calculer_frequentation(queryset, continu=False)
en_continu = self.calculer_frequentation(queryset, continu=True)
data += [(heure, par_heure[heure], en_continu[heure]) for heure in sorted(par_heure.keys())]
else:
data += [("Heure", 0, 0)]
super().__init__(SimpleDataSource(data),
options={
"title": "Fréquentation de la maraude en fonction de l'heure (par quart d'heure)"
}
)
@staticmethod
def calculer_frequentation(observations, step=15, continu=False):
def find_intervalle(temps):
return datetime.time(temps.hour, temps.minute // step * step)
def range_over_intervalles(rencontre):
""" Génère tous les intervalles contenus entre le début et la fin de la rencontre """
curseur = find_intervalle(rencontre.heure_debut)
# Convertir en objet datetime
curseur = datetime.datetime.now().replace(hour=curseur.hour,
minute=curseur.minute)
debut = datetime.datetime.now().replace(hour=rencontre.heure_debut.hour,
minute=rencontre.heure_debut.minute)
fin = debut + datetime.timedelta(0, rencontre.duree * 60)
delta = datetime.timedelta(0, step * 60)
while curseur < fin:
yield datetime.time(curseur.hour, curseur.minute)
curseur += delta
data = collections.defaultdict(lambda: 0)
for rencontre in map(lambda o: o.rencontre, observations):
if not continu:
data[find_intervalle(rencontre.heure_debut)] += 1
else:
for intervalle in range_over_intervalles(rencontre):
data[intervalle] += 1
return data