improve cube view

* float cube form on the right
* fix multiple loading of django-select2
* make the representation choosable between table and charts
* group dimension cells in tables
* add label to y-axis of bar charts
* separte measures into multiple charts
* reduce decimals to 2 in charts
This commit is contained in:
Benjamin Dauvergne 2016-07-21 14:23:00 +02:00
parent c6aa7e30b3
commit 214e825659
4 changed files with 256 additions and 70 deletions

View File

@ -1,3 +1,4 @@
# -*- encoding: utf-8 -*-
# bijoe - BI dashboard
# Copyright (C) 2015 Entr'ouvert
#
@ -27,12 +28,15 @@ except ImportError:
class DateRangeWidget(forms.MultiWidget):
def __init__(self, attrs=None):
attrs2 = {'type': 'date'}
if attrs:
attrs2.update(attrs)
attrs = attrs.copy() if attrs else {}
attrs.update({'type': 'date', 'autocomplete': 'off'})
attrs1 = attrs.copy()
attrs1['placeholder'] = _(u'début')
attrs2 = attrs.copy()
attrs2['placeholder'] = _(u'fin')
widgets = (
forms.DateInput(attrs=attrs2.copy()),
forms.DateInput(attrs=attrs2.copy()),
forms.DateInput(attrs=attrs1),
forms.DateInput(attrs=attrs2),
)
super(DateRangeWidget, self).__init__(widgets, attrs=attrs)
@ -59,6 +63,12 @@ class DateRangeField(forms.MultiValueField):
class CubeForm(forms.Form):
representation = forms.ChoiceField(
label=_(u'Représentation'),
choices=[('table', _('tableau')),
('graphical', _('graphique')),],
widget=forms.RadioSelect())
def __init__(self, *args, **kwargs):
self.cube = cube = kwargs.pop('cube')
super(CubeForm, self).__init__(*args, **kwargs)

View File

@ -119,6 +119,9 @@ LOGGING = {
},
}
# Django-Select2
AUTO_RENDER_SELECT2_STATICS = False
BIJOE_SCHEMAS = []
if 'BIJOE_SETTINGS_FILE' in os.environ:

View File

@ -1,14 +1,45 @@
{% extends "bijoe/base.html" %}
{% load i18n staticfiles %}
{% load i18n staticfiles django_select2_tags %}
{% block extrascripts %}
{% import_django_select2_js_css %}
{{ block.super }}
{{ form.media.css }}
<script src="{% static "js/jquery.form.js" %}"></script>
<script src="{% static "js/addclear.js" %}"></script>
<script src="{% static "xstatic/ChartNew.js" %}"></script>
<style>
form p {
margin-bottom: 0.5ex;
}
form h3 {
margin-top: 0.5ex;
margin-bottom: 0.5ex;
}
.ui-datepicker select {
padding-right: 0px;
}
#id_filter__receipt_time_1 {
margin-left: 16px;
}
#data {
width: calc(100% - 23em);
}
#id_representation {
list-style: none;
padding: 0px;
text-align: justify;
display: flex;
justify-content: space-around;
}
#ods {
margin: 1ex;
top: -2ex;
}
form {
float: left;
width: 20%;
float: right;
width: 20em;
padding-left: 2em;
}
input[type=date] {
width: calc(100% / 2 - 8px);
@ -17,14 +48,39 @@ select {
width: 100%;
}
table.bijoe-table {
td {
vertical-align: top;
}
#data td, #data th {
padding-left: 1ex;
padding-right: 1ex;
}
#data tbody td {
text-align: right;
}
#data tr:hover {
border: 3px solid grey;
}
tbody tr:nth-child(even) td:not([rowspan]) {
background: lightgrey;
}
#data table {
border-collapse: collapse;
line-height: 1.2em;
}
/* table.bijoe-table {
display: flex;
flex-flow: column;
padding-left: 2em;
width: 80%;
height: 70vh;
}
table.bijoe-table thead {
flex: 0 0 auto;
display: table;
@ -38,8 +94,8 @@ table.bijoe-table tr {
table.bijoe-table tbody {
flex: 1 1 auto;
display: block;
overflow-y: auto;
}
overflow-y: auto;
} */
input[type=submit] {
float: right;
z-index: 1;
@ -67,8 +123,17 @@ th {
{% block content %}
<form method="post">
{% csrf_token %}
<input type="submit" value="ODS" name="ods"/>
<h2>Mesure(s)</h2>
<input type="submit" value="ODS" name="ods" id="ods"/>
<h3>Représentation</h3>
{% with field=form.representation %}
<p {% if field.css_classes %}class="{{ field.css_classes }}"{% endif %}>
{{ field }}
{% if field.help_text %}
<span class="helptext">{{ field.help_text }}</span>
{% endif %}
</p>
{% endwith %}
<h3>Mesure(s)</h3>
{% with field=form.measures %}
<p {% if field.css_classes %}class="{{ field.css_classes }}"{% endif %}>
{{ field }}
@ -77,7 +142,7 @@ th {
{% endif %}
</p>
{% endwith %}
<h2>Regroupement(s)</h2>
<h3>Regroupement(s)</h3>
{% with field=form.drilldown %}
<p {% if field.css_classes %}class="{{ field.css_classes }}"{% endif %}>
{{ field }}
@ -86,9 +151,9 @@ th {
{% endif %}
</p>
{% endwith %}
<h2>Filtre(s)</h2
<h3>Filtre(s)</h3
{% for field in form %}
{% if not field.is_hidden and field.name != "measures" and field.name != "drilldown" %}
{% if not field.is_hidden and field.name != "measures" and field.name != "drilldown" and field.name != "representation" %}
<p {% if field.css_classes %}class="{{ field.css_classes }}"{% endif %}>
{{ field.label_tag }}
{% if field.errors %}
@ -106,49 +171,38 @@ th {
{% endif %}
{% endfor %}
</form>
<div id="data">
{% if data %}
<table class="bijoe-table">
<thead>
{% for dimension in drilldown %}
<th><span>{{ dimension.label.capitalize }}</span></th>
{% endfor %}
{% for measure in measures %}
<th><span>{{ measure.label.capitalize }}</span></th>
{% endfor %}
</thead>
<tbody>
{% for row in data %}
<tr>
{% for cell in row %}
<td>{% if cell == None %}{% trans "None" %}{% else %}{{ cell }}{% endif %}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
<canvas style="width: 100%"></canvas>
{% else %}
<div class="big-msg-info">Veuillez choisir des mesures et des regroupements</div>
{% endif %}
{% endblock %}
{% block page-end %}
{{ form.media.js }}
<script>
$(function () {
$('input[type="date"]').datepicker({
dateFormat: 'yy-mm-dd',
changeMonth: true,
changeYear: true,
},
$.datepicker.regional['fr']);
$('input, select').on('change', function () {
$('form').submit();
});
})
{% if data %}
{% if representation == 'table' %}
<table class="bijoe-table">
<thead>
{% for dimension in drilldown %}
<th><span>{{ dimension.label.capitalize }}</span></th>
{% endfor %}
{% for measure in measures %}
<th><span>{{ measure.label.capitalize }}</span></th>
{% endfor %}
</thead>
<tbody>
{% for row in grouped_data %}
<tr>
{% for cell, count in row %}
<td {% if count > 1 %}rowspan="{{ count }}"{% elif count == 0 %}class="measure"{% endif %}>{% if cell == None %}{% trans "None" %}{% else %}{{ cell }}{% endif %}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
{% for measure in measures %}
<h2>{{ measure.label }}</h2>
<canvas style="width: 100%" id="canvas-{{ measure.name }}"></canvas>
<a href="#" download="graph.png" class="download">Download</a>
<script>
console.log('coucou');
var data = {{ json|safe }};
linedata = {labels: [], datasets: []};
var dataset = {data: [], label: "{{ measure.label }}", axis: 1}
var linedata = {labels: [], datasets: [dataset]};
for (var i = 0; i < data.length; i++) {
var row = data[i];
var label = [];
@ -159,15 +213,16 @@ for (var i = 0; i < data.length; i++) {
label = label.join(' ');
linedata.labels.push(label)
for (var j = 0; j < row.measures.length; j++) {
if (datasets.length < j+1) {
datasets.push({data: []});
if (row.measures[j].label != "{{ measure.label }}") {
continue;
}
datasets[j].label = row.measures[j].label;
datasets[j].axis = j+1;
datasets[j].data.push(row.measures[j].value);
dataset.data.push(row.measures[j].value);
}
}
var ctx = $('canvas')[0].getContext("2d");
console.log(linedata);
var ctx = $('#canvas-{{ measure.name }}')[0].getContext("2d");
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, $('canvas')[0].width, $('canvas')[0].height);
var option = {
//Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
scaleBeginAtZero : true,
@ -205,8 +260,77 @@ var ctx = $('canvas')[0].getContext("2d");
legend: true,
yAxisMinimumInterval: 10,
inGraphDataShow: true,
legendFontColor: "rgb(0,0,0,1)",
scaleFontColor: "rgb(0,0,0,1)",
roundNumber: -2,
}
if ("{{ measure.name }}".indexOf("delay") != -1) {
option.yAxisLabel = "jours";
}
if ("{{ measure.name }}" == "count") {
option.yAxisLabel = "quantité";
}
if ("{{ measure.name }}" == "percent") {
option.yAxisLabel = "pourcentage";
}
new Chart(ctx).Bar(linedata, option);
function toDataURL(canvas) {
destinationCanvas = document.createElement("canvas");
destinationCanvas.width = canvas.width;
destinationCanvas.height = canvas.height;
destCtx = destinationCanvas.getContext('2d');
//create a rectangle with the desired color
destCtx.fillStyle = "#FFFFFF";
destCtx.fillRect(0, 0, canvas.width, canvas.height);
//draw the original canvas onto the destination canvas
destCtx.drawImage(canvas, 0, 0);
var data = destinationCanvas.toDataURL("image/png");
data = data.split(";", 2)[1];
data = "data:application/octet-stream;headers=Content-Disposition%3A%20attachment%3B%20filename=graph.png;" + data;
return data
}
$(".download").on('click', function() {
this.href = toDataURL($(this).prev("canvas")[0]);
console.log(this.href);
})
</script>
{% endfor %}
{% endif %}
{% else %}
<div class="big-msg-info">Veuillez choisir des mesures et des regroupements</div>
{% endif %}
</div>
<script>
$(function () {
$('input[type=date]').datepicker({
dateFormat: 'yy-mm-dd',
changeMonth: true,
changeYear: true,
},
$.datepicker.regional['fr']);
$('input, select').on('change', function () {
setTimeout(function () {
$('form').ajaxSubmit({success: function (data) {
var $content = $(data);
$('#data').replaceWith($content.find('#data'));
}});
}, 800);
return true;
});
$('input[type=date]').addClear();
})
</script>
{% endblock %}
{% block page-end %}
{{ form.media.js }}
<script>
{% if data %}
{% endif %}
</script>
{% endblock %}

View File

@ -18,6 +18,9 @@ import json
import datetime
import decimal
import urllib
from collections import OrderedDict
import logging
import hashlib
from django.shortcuts import resolve_url
from django.core.urlresolvers import reverse
@ -28,6 +31,7 @@ from django.utils.translation import ugettext as _
from django.contrib.auth import logout as auth_logout
from django.contrib.auth import views as auth_views
from django.contrib.auth.views import redirect_to_login
from django.core.cache import cache
try:
from mellon.utils import get_idps
@ -75,8 +79,21 @@ class WarehouseView(AuthorizationMixin, TemplateView):
class CubeMixin(object):
def get_key(self, cleaned_data, stringify=True):
measures = cleaned_data.get('measures', [])
drilldown = cleaned_data.get('drilldown', [])
l = []
for kw, values in cleaned_data.iteritems():
if values and kw.startswith('filter__'):
l.append('$'.join([kw] + sorted(map(str, values))))
l += drilldown + sorted(measures)
return hashlib.md5('$'.join(l) + str(stringify)).hexdigest()
def get_data(self, cleaned_data, stringify=True):
cleaned_data = cleaned_data
key = self.get_key(cleaned_data, stringify=stringify)
data = cache.get(key)
if not data is None:
return data
filters = []
for kw, values in cleaned_data.iteritems():
if values and kw.startswith('filter__'):
@ -88,11 +105,11 @@ class CubeMixin(object):
for row in self.cube.query(filters, drilldown, measures):
data_row = []
for cell, value in row:
if stringify:
if cell.type is float:
# FIXME find how to format decimal number using locale with Django
value = ('%05.2f' % float(value)).replace('.', ',') + u' %'
if isinstance(value, datetime.timedelta):
if stringify and cell.type is float:
# FIXME find how to format decimal number using locale with Django
value = ('%05.2f' % float(value)).replace('.', ',') + u' %'
if isinstance(value, datetime.timedelta):
if stringify:
s = ''
if value.days:
s += '%d jour(s)' % value.days
@ -101,10 +118,38 @@ class CubeMixin(object):
if not s:
s = 'moins d\'1 heure'
value = s
else:
value = value.days + value.seconds / 86400.
data_row.append(value)
data.append(data_row)
cache.set(key, data)
return data
def get_grouped_data(self, cleaned_data, stringify=True):
data = self.get_data(cleaned_data, stringify=stringify)
dims = len(cleaned_data.get('drilldown', []))
grouped = OrderedDict()
for row in data:
d = grouped
for dim in row[:dims - 1]:
if dim not in d:
d[dim] = OrderedDict(), 1
else:
d[dim] = d[dim][0], d[dim][1] + 1
d, count = d[dim]
d[row[dims - 1]] = row[dims:], 0
def helper(d, prefix=[]):
for key in d:
row, count = d[key]
if count == 0:
yield prefix + [(key, 1)] + [(v, 0) for v in row]
else:
for r in helper(row, prefix + [(key, count)]):
yield r
prefix = []
return helper(grouped)
class CubeView(AuthorizationMixin, CubeMixin, FormView):
template_name = 'bijoe/cube.html'
@ -153,10 +198,14 @@ class CubeView(AuthorizationMixin, CubeMixin, FormView):
ctx['cube'] = self.cube
form = ctx['form']
ctx['data'] = False
if form.is_valid():
ctx['representation'] = form.cleaned_data['representation']
ctx['data'] = self.get_data(form.cleaned_data)
ctx['grouped_data'] = list(self.get_grouped_data(form.cleaned_data))
ctx['measures'] = [self.cube.measures[measure] for measure in
form.cleaned_data['measures']]
ctx['measures_json'] = json.dumps([{'name': measure.name, 'label': measure.label} for measure in ctx['measures']])
ctx['drilldown'] = [self.cube.dimensions[dimension] for dimension in
form.cleaned_data['drilldown']]
json_data = []