modified MultipleChartViews to instantiate only one chart.
added chart on index view
This commit is contained in:
@@ -1,22 +1,48 @@
|
||||
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=[]):
|
||||
def __init__(self, queryset, field, labels=None, excluded=None):
|
||||
self.queryset = queryset
|
||||
self.field_name = field.name
|
||||
self.excluded = excluded
|
||||
self.excluded = excluded or []
|
||||
if not labels:
|
||||
if field.__class__ in LABELS:
|
||||
labels = LABELS[field.__class__]
|
||||
@@ -52,19 +78,22 @@ class PieWrapper(gchart.PieChart):
|
||||
- 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=[],
|
||||
null_values=None,
|
||||
**kwargs):
|
||||
if not data:
|
||||
if data is None:
|
||||
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")
|
||||
data_source = FieldValuesCountDataSource(
|
||||
queryset, field,
|
||||
excluded=null_values,
|
||||
@@ -73,14 +102,12 @@ class PieWrapper(gchart.PieChart):
|
||||
else:
|
||||
data_source = SimpleDataSource(data=data)
|
||||
|
||||
super().__init__(
|
||||
data_source,
|
||||
options={
|
||||
'title': getattr(field, 'verbose_name', title),
|
||||
'is3D': True,
|
||||
'pieSliceText': 'value',
|
||||
'legend': {'position': 'labeled', 'maxLines': 3, 'textStyle': {'fontSize': 16,}},
|
||||
},
|
||||
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),
|
||||
)
|
||||
@@ -94,4 +121,176 @@ class PieWrapper(gchart.PieChart):
|
||||
|
||||
class ColumnWrapper(gchart.ColumnChart):
|
||||
|
||||
pass
|
||||
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
|
||||
|
||||
@@ -32,11 +32,4 @@ def get_year_range():
|
||||
|
||||
class SelectRangeForm(forms.Form):
|
||||
|
||||
year = forms.ChoiceField(label="Année", choices=[(0, 'Toutes')] + [(i, str(i)) for i in get_year_range()])
|
||||
month = forms.ChoiceField(label="Mois",
|
||||
choices=[(0, 'Tous'),
|
||||
(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')
|
||||
],
|
||||
)
|
||||
period = forms.ChoiceField(label="Année", choices=[(0, 'Tout')] + [(i, str(i)) for i in get_year_range()])
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
<h4>Période</h4>
|
||||
<form action="" method="get">
|
||||
{% bootstrap_form form layout="inline" %}
|
||||
<input type="hidden" name="graph" value="{{active}}" />
|
||||
{% bootstrap_button "Ok" button_type="submit" %}
|
||||
</form>
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
<div class="text-center">
|
||||
<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,6 +6,7 @@
|
||||
|
||||
{% block sidebar %}
|
||||
{{ block.super }}
|
||||
<hr />
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-body text-right">
|
||||
{% include "statistiques/filter_form.html" %}
|
||||
@@ -19,22 +20,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
|
||||
<div class="col-lg-8 text-right">
|
||||
<h3 class="page-header">Données générales</h3>
|
||||
<table class="table table-bordered">
|
||||
<tr><th>...</th><th>Maraudes</th><th>Nombre de rencontres <span class="badge">moyenne par maraude</span></th><th>Personnes</th></tr>
|
||||
<tr><th>Total</th><td>{{nbr_maraudes}}</td><td>{{nbr_rencontres}} <span class="badge">{{nbr_rencontres_moyenne }}</span></td><td>{{nbr_sujets}}</td></tr>
|
||||
<tr><th>Soirée</th><td>{{nbr_maraudes_nuit}}</td><td>{{nbr_rencontres_nuit}} <span class="badge">{{nbr_rencontres_nuit_moyenne }}</span></td><td>{{nbr_sujets_nuit}}</td></tr>
|
||||
<tr><th>Journée</th><td>{{nbr_maraudes_jour}}</td><td>{{nbr_rencontres_jour}} <span class="badge">{{nbr_rencontres_jour_moyenne }}</span></td><td>{{nbr_sujets_jour}}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="alert alert-info">
|
||||
<p>Voici les données permettant une analyse statistiques des maraudes.</p>
|
||||
<p>Vous pouvez sélectionner une période particulière ou l'ensemble des données</p>
|
||||
<p>Les données sont réparties en trois catégories, accessibles par le menu sur la gauche</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ chart.as_html }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
{% load navbar %}
|
||||
<ul class="nav nav-pills nav-stacked text-right">
|
||||
<li role="presentation" {% active namespace="statistiques" viewname="index" %}>
|
||||
<a href="{% url "statistiques:index" %}?year={{year|default:0}}{% if month %}&month={{month}}{% endif %}">Maraudes
|
||||
<a href="{% url "statistiques:index" %}?period={{year|default:0}}">Maraudes
|
||||
<span class="glyphicon glyphicon-road"></span>
|
||||
</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
|
||||
<li role="presentation" {% active namespace="statistiques" viewname="typologie" %}>
|
||||
<a href="{% url "statistiques:typologie" %}?period={{year|default:0}}">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
|
||||
<a href="{% url "statistiques:frequentation" %}?period={{year|default:0}}">Fréquentation
|
||||
<span class="glyphicon glyphicon-stats"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr />
|
||||
|
||||
@@ -2,42 +2,37 @@
|
||||
|
||||
{% block title %}{{block.super}} {{page_title}}{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}{{block.super}}<li>{{page_title}}</li>{% endblock %}
|
||||
{% block breadcrumbs %}
|
||||
{{block.super}}
|
||||
<li>{{page_title}}</li>
|
||||
{% if active %}<li>{{active}}</li>{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{{ block.super }}
|
||||
{% if chart %}
|
||||
<hr />
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-body text-right">
|
||||
{% include "statistiques/filter_form.html" %}
|
||||
{% include "statistiques/filter_form.html" with active=active %}
|
||||
<hr />
|
||||
<p>Échantillon : {{ queryset_count }} objets</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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 class="nav nav-pills nav-justified">
|
||||
{% for name in chart_list %}
|
||||
<li role="presentation" {%if name == active%} class="active" {%endif%}>
|
||||
<a href="?graph={{name}}&period={{year}}">{{ name }}</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 %}
|
||||
{% if chart %}
|
||||
{{ chart.as_html }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,9 +4,8 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url('^$', views.DashboardView.as_view(), name="index"),
|
||||
url('^charts/$', views.PieChartView.as_view(), name="pies"),
|
||||
url('^charts/$', views.TypologieChartsView.as_view(), name="typologie"),
|
||||
url(r'^details/(?P<pk>[0-9]+)/$', views.StatistiquesDetailsView.as_view(), name="details"),
|
||||
url(r'^update/(?P<pk>[0-9]+)/$', views.StatistiquesUpdateView.as_view(), name="update"),
|
||||
url(r'^frequentation/$', views.FrequentationStatsView.as_view(), name="frequentation"),
|
||||
url(r'^frequentation/$', views.FrequentationChartsView.as_view(), name="frequentation"),
|
||||
]
|
||||
|
||||
|
||||
@@ -1,64 +1,43 @@
|
||||
import datetime
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.shortcuts import redirect
|
||||
from django.views import generic
|
||||
from django.db.models import (CharField, NullBooleanField,
|
||||
Count,
|
||||
)
|
||||
from django.db.models.functions.datetime import ExtractMonth
|
||||
from graphos.sources.simple import SimpleDataSource
|
||||
from graphos.renderers import gchart
|
||||
|
||||
from .models import FicheStatistique
|
||||
from .forms import StatistiquesForm, SelectRangeForm
|
||||
from .charts import PieWrapper, ColumnWrapper
|
||||
from django.db.models import (CharField, NullBooleanField)
|
||||
|
||||
from maraudes.notes import Observation
|
||||
from maraudes.models import Maraude, HORAIRES_APRESMIDI, HORAIRES_SOIREE
|
||||
from notes.models import Sujet
|
||||
|
||||
from .models import FicheStatistique
|
||||
from .forms import StatistiquesForm, SelectRangeForm
|
||||
from . import charts
|
||||
###
|
||||
|
||||
NO_DATA = "Aucune donnée"
|
||||
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"
|
||||
}
|
||||
|
||||
|
||||
class FilterMixin(generic.edit.FormMixin):
|
||||
class FilterMixin(generic.edit.FormView):
|
||||
|
||||
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),
|
||||
'period': self.year,
|
||||
}
|
||||
|
||||
def parse_args(self, request):
|
||||
self.year = int(request.GET.get('year', 0))
|
||||
self.month = int(request.GET.get('month', 0))
|
||||
period = request.GET.get("period", "0")
|
||||
self.year = int(period)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.parse_args(request)
|
||||
return super().get(self, *args, **kwargs)
|
||||
|
||||
def _filters(self, prefix):
|
||||
return {'%s__%s' % (prefix, attr): getattr(self, attr) for attr in ('year', 'month')
|
||||
return {'%s__%s' % (prefix, attr): getattr(self, attr) for attr in ('year',)
|
||||
if getattr(self, attr) > 0}
|
||||
|
||||
def get_observations_queryset(self):
|
||||
@@ -68,7 +47,7 @@ class FilterMixin(generic.edit.FormMixin):
|
||||
return Maraude.objects.filter(**self._filters('date'))
|
||||
|
||||
def get_fichestatistiques_queryset(self):
|
||||
return FicheStatistique.objects.filter(pk__in=self.get_observations_queryset().values_list('sujet'))
|
||||
return FicheStatistique.objects.filter(pk__in=self.get_sujets_queryset().values_list('pk'))
|
||||
|
||||
def get_sujets_queryset(self, selection=None):
|
||||
if not selection:
|
||||
@@ -78,29 +57,43 @@ class FilterMixin(generic.edit.FormMixin):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['year'] = self.year
|
||||
context['month'] = self.month
|
||||
return context
|
||||
|
||||
|
||||
class MultipleChartsView(FilterMixin, generic.TemplateView):
|
||||
|
||||
title = None
|
||||
description = None
|
||||
_charts = {} # Set of charts managed by the view
|
||||
|
||||
_active = None # Name of the active chart
|
||||
_chart = None # Active chart object
|
||||
template_name = "statistiques/multiple_charts.html"
|
||||
page_title = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if not self._charts:
|
||||
raise ImproperlyConfigured()
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self._active = request.GET.get('graph', None)
|
||||
if self._active:
|
||||
self._chart = self._charts.get(self._active, None)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
""" Returns the queryset of objects used to draw graphs """
|
||||
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)
|
||||
context['page_title'] = str(self.title)
|
||||
context['chart_list'] = list(self._charts.keys())
|
||||
context['active'] = self._active
|
||||
if self._chart: # Need to instantiate the chart
|
||||
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)
|
||||
]
|
||||
context['chart'] = self._chart(queryset)
|
||||
return context
|
||||
|
||||
|
||||
@@ -113,196 +106,49 @@ class DashboardView(FilterMixin, generic.TemplateView):
|
||||
maraudes = self.get_maraudes_queryset()
|
||||
rencontres = self.get_observations_queryset()
|
||||
|
||||
context['nbr_maraudes'] = maraudes.count() or NO_DATA
|
||||
context['nbr_maraudes_jour'] = maraudes.filter(
|
||||
heure_debut=HORAIRES_APRESMIDI
|
||||
).count() or NO_DATA
|
||||
context['nbr_maraudes_nuit'] = maraudes.filter(
|
||||
heure_debut=HORAIRES_SOIREE
|
||||
).count() or NO_DATA
|
||||
|
||||
context['nbr_rencontres'] = rencontres.count() or NO_DATA
|
||||
rencontres_jour = rencontres.filter(
|
||||
rencontre__maraude__heure_debut=HORAIRES_APRESMIDI
|
||||
context['chart'] = charts.DonneeGeneraleChart(
|
||||
maraudes=(maraudes,
|
||||
maraudes.filter(heure_debut=HORAIRES_APRESMIDI),
|
||||
maraudes.filter(heure_debut=HORAIRES_SOIREE)
|
||||
),
|
||||
rencontres=(rencontres,
|
||||
rencontres.filter(rencontre__maraude__heure_debut=HORAIRES_APRESMIDI),
|
||||
rencontres.filter(rencontre__maraude__heure_debut=HORAIRES_SOIREE)),
|
||||
)
|
||||
rencontres_nuit = rencontres.filter(
|
||||
rencontre__maraude__heure_debut=HORAIRES_SOIREE
|
||||
)
|
||||
context['nbr_rencontres_jour'] = rencontres_jour.count() or NO_DATA
|
||||
context['nbr_rencontres_nuit'] = rencontres_nuit.count() or NO_DATA
|
||||
|
||||
for r, m in [
|
||||
('nbr_rencontres', 'nbr_maraudes'),
|
||||
('nbr_rencontres_nuit', 'nbr_maraudes_nuit'),
|
||||
('nbr_rencontres_jour', 'nbr_maraudes_jour'),
|
||||
]:
|
||||
try:
|
||||
context['%s_moyenne' % r] = int(context[r] / context[m])
|
||||
except (ZeroDivisionError, TypeError):
|
||||
context['%s_moyenne' % r] = NO_DATA
|
||||
|
||||
context['nbr_sujets'] = self.get_sujets_queryset(selection=rencontres).count()
|
||||
context['nbr_sujets_jour'] = self.get_sujets_queryset(selection=rencontres_jour).count()
|
||||
context['nbr_sujets_nuit'] = self.get_sujets_queryset(selection=rencontres_nuit).count()
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class PieChartView(MultipleChartsView):
|
||||
page_title = "Typologie"
|
||||
class TypologieChartsView(MultipleChartsView):
|
||||
title = "Typologie"
|
||||
|
||||
_charts = OrderedDict(
|
||||
[('Âge', charts.AgePieChart),
|
||||
('Genre', charts.GenrePieChart),
|
||||
] +
|
||||
[(f.verbose_name, charts.generate_piechart_for_field(f)) for f in FicheStatistique._meta.get_fields()
|
||||
if f.__class__ in (NullBooleanField, CharField)
|
||||
]
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_fichestatistiques_queryset()
|
||||
|
||||
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:
|
||||
if field.name == 'genre':
|
||||
yield str(field.verbose_name), PieWrapper(sujets, field)
|
||||
if field.name == 'age':
|
||||
categories = (
|
||||
('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)),
|
||||
)
|
||||
nbr_sujets = lambda rg: sujets.filter(age__in=rg).count()
|
||||
|
||||
yield "Âge", PieWrapper(
|
||||
data=[("age", "count")] +
|
||||
[(label, nbr_sujets(rg))
|
||||
for label, rg in categories] +
|
||||
[("Ne sait pas", sujets.filter(age=None).count())],
|
||||
title="Âge des sujets")
|
||||
|
||||
# Puis des champs du modèle statistiques.FicheStatistique
|
||||
# dans leur ordre de déclaration
|
||||
for field in FicheStatistique._meta.fields:
|
||||
if field.__class__ in (NullBooleanField, CharField):
|
||||
yield str(field.verbose_name), PieWrapper(queryset, field)
|
||||
|
||||
|
||||
class FrequentationStatsView(MultipleChartsView):
|
||||
page_title = "Fréquentation"
|
||||
|
||||
@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
|
||||
class FrequentationChartsView(MultipleChartsView):
|
||||
title = "Fréquentation"
|
||||
_charts = OrderedDict([
|
||||
('Par mois', charts.RencontreParMoisChart),
|
||||
('Par heure', charts.RencontreParHeureChart),
|
||||
('Par sujet', charts.RencontreParSujetChart),
|
||||
])
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_observations_queryset()
|
||||
|
||||
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())]
|
||||
),
|
||||
options={
|
||||
"title": "Fréquentation de la maraude en fonction de l'heure (par quart d'heure)"
|
||||
}
|
||||
)))
|
||||
|
||||
# Nombre de rencontres en fonction du mois
|
||||
par_mois = queryset.annotate(
|
||||
mois=ExtractMonth('created_date')
|
||||
).values(
|
||||
'mois'
|
||||
).annotate(
|
||||
nbr=Count('pk')
|
||||
).order_by()
|
||||
graphs.append(("Par mois", ColumnWrapper(
|
||||
SimpleDataSource(
|
||||
[("Mois", "Rencontres")] +
|
||||
[(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 = queryset.values('sujet').annotate(nbr=Count('pk')).order_by()
|
||||
categories = (
|
||||
('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)),
|
||||
)
|
||||
get_count_for_range = lambda rg: nbr_rencontres.filter(nbr__in=rg).count()
|
||||
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'
|
||||
)))
|
||||
|
||||
# Rencontres par lieu
|
||||
# TODO: More customizable way of categorizing "Lieu"
|
||||
|
||||
return graphs
|
||||
|
||||
|
||||
# AjaxMixin
|
||||
class AjaxOrRedirectMixin:
|
||||
class AjaxOrRedirectMixin(generic.DetailView):
|
||||
""" For view that should be retrieved by Ajax only. If not,
|
||||
redirects to the primary view where these are displayed """
|
||||
|
||||
@@ -313,13 +159,13 @@ class AjaxOrRedirectMixin:
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
|
||||
class StatistiquesDetailsView(AjaxOrRedirectMixin, generic.DetailView):
|
||||
class StatistiquesDetailsView(AjaxOrRedirectMixin):
|
||||
|
||||
model = FicheStatistique
|
||||
template_name = "statistiques/fiche_stats_details.html"
|
||||
|
||||
|
||||
class StatistiquesUpdateView(AjaxOrRedirectMixin, generic.UpdateView):
|
||||
class StatistiquesUpdateView(AjaxOrRedirectMixin):
|
||||
|
||||
model = FicheStatistique
|
||||
form_class = StatistiquesForm
|
||||
|
||||
Reference in New Issue
Block a user