import datetime 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_par_quart_heure(queryset, continu=False) en_continu = self.calculer_frequentation_par_quart_heure(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_par_quart_heure(observations, continu=False): """ Calcule le nombre d'observations, de 16h à 24h, par tranche de 15min. L'algorithme est *très peu* efficace mais simple à comprendre : on calcule pour chaque tranche les observations qui y sont contenues. On peut calculer seulement les observations démarrées (continu = False) ou considérer que l'observation est contenue dans un intervalle sur toute sa durée (continu = True). """ data = dict() def genere_filtre_pour(heure, indice): """ Renvoie une fonction qui renvoie True si l'intervalle donné contient l'observation, c'est-à-dire : 1. Elle démarre/finit dans l'intervalle. 2. Elle démarre avant et fini après l'intervalle. """ debut_intervalle = indice * 15 fin_intervalle = debut_intervalle + 15 rng = range(debut_intervalle, fin_intervalle) def est_contenue(observation): """ Vérifie l'observation est contenue dans l'intervalle """ debut = datetime.datetime.strptime( "%s" % observation.rencontre.heure_debut, "%H:%M:%S" ) fin = debut + datetime.timedelta(0, observation.rencontre.duree * 60) # L'observation démarre dans l'intervalle if debut.hour == heure and debut.minute in rng: return True # L'observation finit dans l'intervalle, seulement si continu est True elif continu and (fin.hour == heure and fin.minute in rng): return True # L'observation démarre avant ET finit après l'intervalle, # seulement si continu est True elif (continu and (debut.hour <= heure and debut.minute <= debut_intervalle) and (fin.hour >= heure and fin.minute >= fin_intervalle)): return True else: return False return est_contenue for h in range(16, 24): for i in range(4): filtre = genere_filtre_pour(heure=h, indice=i) contenus = list(filter(filtre, observations)) key = datetime.time(h, i * 15) data[key] = len(contenus) return data