created MultipleChartsView
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
# Requirements
|
# Requirements
|
||||||
|
|
||||||
|
django
|
||||||
django-bootstrap3
|
django-bootstrap3
|
||||||
django-select2
|
django-select2
|
||||||
django-watson
|
django-watson
|
||||||
|
|||||||
@@ -1,61 +1,88 @@
|
|||||||
from django.db.models import (Field, CharField, NullBooleanField,
|
from django.db.models import (Field, NullBooleanField,
|
||||||
Count,
|
Count,
|
||||||
)
|
)
|
||||||
from graphos.sources.simple import SimpleDataSource
|
from graphos.sources.simple import SimpleDataSource
|
||||||
from graphos.renderers import gchart
|
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):
|
class PieWrapper(gchart.PieChart):
|
||||||
""" A wrapper around gchart.PieChart that generates a graph from :
|
""" A wrapper around gchart.PieChart that generates a graph from :
|
||||||
|
|
||||||
- a queryset and a model field (NullBooleanField or field with choices)
|
- a queryset and a model field (NullBooleanField or field with choices)
|
||||||
|
OR
|
||||||
- a data object and title
|
- a data object and title
|
||||||
"""
|
"""
|
||||||
|
|
||||||
height=400
|
height=400
|
||||||
width=800
|
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,
|
data=None, title=None,
|
||||||
null_values=[],**kwargs):
|
null_values=[],
|
||||||
|
**kwargs):
|
||||||
if not data:
|
if not data:
|
||||||
if not isinstance(field, Field):
|
if not isinstance(field, Field):
|
||||||
raise TypeError(field, 'must be a child of django.models.db.fields.Field !')
|
raise TypeError(field, 'must be a child of django.models.db.fields.Field !')
|
||||||
if not queryset:
|
if not queryset:
|
||||||
raise TypeError("You must give either a queryset and field or data")
|
raise TypeError("You must give either a queryset and field or data")
|
||||||
|
data_source = FieldValuesCountDataSource(
|
||||||
if field.__class__ in PieWrapper.labels:
|
queryset, field,
|
||||||
labels = PieWrapper.labels[field.__class__]
|
excluded=null_values,
|
||||||
elif field.choices:
|
labels=None #TODO: How to pass in labels ??
|
||||||
labels = dict(field.choices)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Could not guess labels for", field)
|
data_source = SimpleDataSource(data=data)
|
||||||
|
|
||||||
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)
|
|
||||||
])
|
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
SimpleDataSource(
|
data_source,
|
||||||
data=data
|
|
||||||
),
|
|
||||||
options={
|
options={
|
||||||
'title': getattr(field, 'verbose_name', title),
|
'title': getattr(field, 'verbose_name', title),
|
||||||
'is3D': True,
|
'is3D': True,
|
||||||
'pieSliceText': 'value',
|
'pieSliceText': 'value',
|
||||||
'legend': {'position': 'labeled', 'maxLines': 3, 'textStyle': {'fontSize': 16,}},
|
'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):
|
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>
|
<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>
|
<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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation" {% active namespace="statistiques" viewname="pies" %}>
|
<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
|
<a href="{% url "statistiques:pies" %}?year={{year|default:0}}{% if month %}&month={{month}}{% endif %}">Typologie
|
||||||
<span class="glyphicon glyphicon-adjust"></span>
|
<span class="glyphicon glyphicon-user"></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation" {% active namespace="statistiques" viewname="frequentation" %}>
|
<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" %}?year={{year|default:0}}{% if month %}&month={{month}}{% endif %}">Fréquentation
|
||||||
<span class="glyphicon glyphicon-adjust"></span>
|
<span class="glyphicon glyphicon-stats"></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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
|
import datetime
|
||||||
|
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import redirect
|
||||||
from django.contrib import messages
|
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.db.models import (Field, CharField, NullBooleanField,
|
from django.db.models import (CharField, NullBooleanField,
|
||||||
Count,
|
Count,
|
||||||
)
|
)
|
||||||
from django.db.models.functions.datetime import ExtractMonth
|
from django.db.models.functions.datetime import ExtractMonth
|
||||||
@@ -20,8 +19,8 @@ from notes.models import Sujet
|
|||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
|
NO_DATA = "Aucune donnée"
|
||||||
nom_mois = {
|
NOM_MOIS = {
|
||||||
1: "Janvier",
|
1: "Janvier",
|
||||||
2: "Février",
|
2: "Février",
|
||||||
3: "Mars",
|
3: "Mars",
|
||||||
@@ -40,13 +39,22 @@ nom_mois = {
|
|||||||
class FilterMixin(generic.edit.FormMixin):
|
class FilterMixin(generic.edit.FormMixin):
|
||||||
|
|
||||||
form_class = SelectRangeForm
|
form_class = SelectRangeForm
|
||||||
|
request = None
|
||||||
|
year = None
|
||||||
|
month = None
|
||||||
|
|
||||||
def get_initial(self):
|
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):
|
def parse_args(self, request):
|
||||||
self.year = int(self.request.GET.get('year', 0))
|
self.year = int(request.GET.get('year', 0))
|
||||||
self.month = int(self.request.GET.get('month', 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)
|
return super().get(self, *args, **kwargs)
|
||||||
|
|
||||||
def _filters(self, prefix):
|
def _filters(self, prefix):
|
||||||
@@ -74,7 +82,27 @@ class FilterMixin(generic.edit.FormMixin):
|
|||||||
return context
|
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):
|
class DashboardView(FilterMixin, generic.TemplateView):
|
||||||
template_name = "statistiques/index.html"
|
template_name = "statistiques/index.html"
|
||||||
@@ -120,11 +148,13 @@ class DashboardView(FilterMixin, generic.TemplateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class PieChartView(MultipleChartsView):
|
||||||
|
page_title = "Typologie"
|
||||||
|
|
||||||
class PieChartView(FilterMixin, generic.TemplateView):
|
def get_queryset(self):
|
||||||
template_name = "statistiques/typologie.html"
|
return self.get_fichestatistiques_queryset()
|
||||||
|
|
||||||
def get_graphs(self):
|
def get_graphs(self, queryset):
|
||||||
sujets = self.get_sujets_queryset()
|
sujets = self.get_sujets_queryset()
|
||||||
# Insertion des champs 'âge' et 'genre' du modèle notes.Sujet
|
# Insertion des champs 'âge' et 'genre' du modèle notes.Sujet
|
||||||
for field in Sujet._meta.fields:
|
for field in Sujet._meta.fields:
|
||||||
@@ -150,22 +180,13 @@ class PieChartView(FilterMixin, generic.TemplateView):
|
|||||||
|
|
||||||
# Puis des champs du modèle statistiques.FicheStatistique
|
# Puis des champs du modèle statistiques.FicheStatistique
|
||||||
# dans leur ordre de déclaration
|
# dans leur ordre de déclaration
|
||||||
queryset = self.get_fichestatistiques_queryset()
|
|
||||||
for field in FicheStatistique._meta.fields:
|
for field in FicheStatistique._meta.fields:
|
||||||
if field.__class__ in (NullBooleanField, CharField):
|
if field.__class__ in (NullBooleanField, CharField):
|
||||||
yield str(field.verbose_name), PieWrapper(queryset, field)
|
yield str(field.verbose_name), PieWrapper(queryset, field)
|
||||||
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
class FrequentationStatsView(MultipleChartsView):
|
||||||
context = super().get_context_data(**kwargs)
|
page_title = "Fréquentation"
|
||||||
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"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def calculer_frequentation_par_quart_heure(observations, continu=False):
|
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 (debut.hour <= heure and debut.minute <= debut_intervalle)
|
||||||
and (fin.hour >= heure and fin.minute >= fin_intervalle)):
|
and (fin.hour >= heure and fin.minute >= fin_intervalle)):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -224,64 +244,64 @@ class FrequentationStatsView(FilterMixin, generic.TemplateView):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.get_observations_queryset()
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_graphs(self, queryset):
|
||||||
context = super().get_context_data(**kwargs)
|
graphs = []
|
||||||
|
# Nombre de rencontres en fonction de l'heure
|
||||||
observations = self.get_observations_queryset()
|
par_heure = self.calculer_frequentation_par_quart_heure(queryset, continu=False)
|
||||||
|
en_continu = self.calculer_frequentation_par_quart_heure(queryset, continu=True)
|
||||||
par_heure = self.calculer_frequentation_par_quart_heure(observations, continu=False)
|
graphs.append(("Par heure", gchart.AreaChart(
|
||||||
en_continu = self.calculer_frequentation_par_quart_heure(observations, continu=True)
|
|
||||||
|
|
||||||
context['rencontres_par_heure'] = gchart.AreaChart(
|
|
||||||
SimpleDataSource(
|
SimpleDataSource(
|
||||||
[("Heure", "Rencontres démarrées", "Au total (démarré + en cours)")] +
|
[("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())]
|
[(heure, par_heure[heure], en_continu[heure]) for heure in sorted(par_heure.keys())]
|
||||||
),
|
),
|
||||||
options = {
|
options={
|
||||||
"title": "Fréquentation de la maraude en fonction de l'heure (par quart d'heure)"
|
"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')
|
mois=ExtractMonth('created_date')
|
||||||
).values(
|
).values(
|
||||||
'mois'
|
'mois'
|
||||||
).annotate(
|
).annotate(
|
||||||
nbr=Count('pk')
|
nbr=Count('pk')
|
||||||
)
|
).order_by()
|
||||||
context['rencontres_par_mois'] = ColumnWrapper(
|
graphs.append(("Par mois", ColumnWrapper(
|
||||||
SimpleDataSource(
|
SimpleDataSource(
|
||||||
[("Mois", "Rencontres")] +
|
[("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 = {
|
options={
|
||||||
"title": "Nombre de rencontres par mois"
|
"title": "Nombre de rencontres par mois"
|
||||||
}
|
}
|
||||||
)
|
)))
|
||||||
|
|
||||||
# Graph: Fréquence de rencontres par sujet
|
# Graph: Fréquence de rencontres par sujet
|
||||||
nbr_rencontres = observations.values('sujet').annotate(nbr=Count('pk')).order_by()
|
nbr_rencontres = queryset.values('sujet').annotate(nbr=Count('pk')).order_by()
|
||||||
context['rencontres_par_sujet'] = nbr_rencontres.count()
|
|
||||||
|
|
||||||
categories = (
|
categories = (
|
||||||
('Rencontre unique', (1,)),
|
('Rencontre unique', (1,)),
|
||||||
('Entre 2 et 5 rencontres', range(2,6)),
|
('Entre 2 et 5 rencontres', range(2, 6)),
|
||||||
('Entre 6 et 20 rencontres', range(6,20)),
|
('Entre 6 et 20 rencontres', range(6, 20)),
|
||||||
('Plus de 20 rencontres', range(20,999)),
|
('Plus de 20 rencontres', range(20, 999)),
|
||||||
)
|
)
|
||||||
get_count_for_range = lambda rg: nbr_rencontres.filter(nbr__in=rg).count()
|
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')] +
|
data=[('Type de rencontre', 'Nombre de sujets')] +
|
||||||
[(label, get_count_for_range(rg)) for label, rg in categories],
|
[(label, get_count_for_range(rg)) for label, rg in categories],
|
||||||
title= 'Fréquence de rencontres'
|
title='Fréquence de rencontres'
|
||||||
)
|
)))
|
||||||
|
|
||||||
return context
|
# Rencontres par lieu
|
||||||
|
# TODO: More customizable way of categorizing "Lieu"
|
||||||
|
|
||||||
|
return graphs
|
||||||
|
|
||||||
|
|
||||||
# AjaxMixin
|
# AjaxMixin
|
||||||
|
|
||||||
class AjaxOrRedirectMixin:
|
class AjaxOrRedirectMixin:
|
||||||
""" For view that should be retrieved by Ajax only. If not,
|
""" For view that should be retrieved by Ajax only. If not,
|
||||||
redirects to the primary view where these are displayed """
|
redirects to the primary view where these are displayed """
|
||||||
@@ -293,14 +313,12 @@ class AjaxOrRedirectMixin:
|
|||||||
return super().get(*args, **kwargs)
|
return super().get(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class StatistiquesDetailsView(AjaxOrRedirectMixin, generic.DetailView):
|
class StatistiquesDetailsView(AjaxOrRedirectMixin, generic.DetailView):
|
||||||
|
|
||||||
model = FicheStatistique
|
model = FicheStatistique
|
||||||
template_name = "statistiques/fiche_stats_details.html"
|
template_name = "statistiques/fiche_stats_details.html"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class StatistiquesUpdateView(AjaxOrRedirectMixin, generic.UpdateView):
|
class StatistiquesUpdateView(AjaxOrRedirectMixin, generic.UpdateView):
|
||||||
|
|
||||||
model = FicheStatistique
|
model = FicheStatistique
|
||||||
|
|||||||
Reference in New Issue
Block a user