created MultipleChartsView

This commit is contained in:
artus40
2017-08-18 14:00:59 +02:00
parent c8c59b92a2
commit 6d2ce7d7bc
6 changed files with 189 additions and 103 deletions

View File

@@ -1,5 +1,6 @@
# Requirements
django
django-bootstrap3
django-select2
django-watson

View File

@@ -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):

View File

@@ -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>

View File

@@ -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&nbsp;
<span class="glyphicon glyphicon-adjust"></span>
<a href="{% url "statistiques:pies" %}?year={{year|default:0}}{% if month %}&month={{month}}{% endif %}">Typologie&nbsp;
<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&nbsp;
<span class="glyphicon glyphicon-adjust"></span>
<span class="glyphicon glyphicon-stats"></span>
</a>
</li>
</ul>

View 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 %}

View File

@@ -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