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:
parent
c6aa7e30b3
commit
214e825659
|
@ -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)
|
||||
|
|
|
@ -119,6 +119,9 @@ LOGGING = {
|
|||
},
|
||||
}
|
||||
|
||||
# Django-Select2
|
||||
AUTO_RENDER_SELECT2_STATICS = False
|
||||
|
||||
BIJOE_SCHEMAS = []
|
||||
|
||||
if 'BIJOE_SETTINGS_FILE' in os.environ:
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 = []
|
||||
|
|
Loading…
Reference in New Issue