improve cube view

* make it compatible with django-select2 >=5 by implemting a dummy templatetag
* use jquery.form.js to submit cube settings
* use addclear.js to clear data fields
* make one chart by measure
* use pie charts for percent measure
* add automatic coloring of charts
* use a button style for the Download PNG link of charts
* add URL to bookmark cube settings
This commit is contained in:
Benjamin Dauvergne 2016-07-22 15:54:50 +02:00
parent 4d45677cf1
commit 69fb738818
8 changed files with 1652 additions and 69 deletions

View File

@ -22,9 +22,15 @@ from django.utils.translation import ugettext as _
try:
from django_select2.forms import Select2MultipleWidget
def build_select2_widget():
return Select2MultipleWidget()
except ImportError:
from django_select2.widgets import Select2MultipleWidget
def build_select2_widget():
return Select2MultipleWidget(select2_options={'width': 'resolve'})
class DateRangeWidget(forms.MultiWidget):
def __init__(self, attrs=None):
@ -86,17 +92,17 @@ class CubeForm(forms.Form):
label=dimension.label.capitalize(),
choices=dimension.members,
required=False,
widget=Select2MultipleWidget(select2_options={'width': 'resolve'}))
widget=build_select2_widget())
# group by
choices = [(dimension.name, dimension.label) for dimension in cube.dimensions
if dimension.type not in (datetime.datetime, datetime.date)]
self.fields['drilldown'] = forms.MultipleChoiceField(
label=_('Group by'), choices=choices, required=False,
widget=Select2MultipleWidget(select2_options={'width': 'resolve'}))
widget=build_select2_widget())
# measures
choices = [(measure.name, measure.label) for measure in cube.measures]
self.fields['measures'] = forms.MultipleChoiceField(
label=_('Measures'), choices=choices,
widget=Select2MultipleWidget(select2_options={'width': 'resolve'}))
widget=build_select2_widget())

175
bijoe/static/js/addclear.js Normal file
View File

@ -0,0 +1,175 @@
/*!
Author: Stephen Korecky
Website: http://stephenkorecky.com
Plugin Website: http://github.com/skorecky/Add-Clear
Version: 2.0.6
The MIT License (MIT)
Copyright (c) 2015 Stephen Korecky
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
;(function($, window, document, undefined) {
// Create the defaults once
var pluginName = "addClear",
defaults = {
closeSymbol: "✖",
color: "#CCC",
top: 1,
right: 4,
returnFocus: false,
showOnLoad: false,
onClear: null,
hideOnBlur: false,
tabbable: true,
paddingRight: '20px'
};
// The actual plugin constructor
function Plugin(element, options) {
this.element = element;
this.options = $.extend({}, defaults, options);
this._defaults = defaults;
this._name = pluginName;
this.init();
}
Plugin.prototype = {
init: function() {
var $this = $(this.element),
$clearButton,
me = this,
options = this.options;
$this.wrap("<span style='position:relative;' class='add-clear-span'></span>");
var tabIndex = options.tabbable ? "" : " tabindex='-1'";
$clearButton = $("<a href='#clear' style='display: none;'" + tabIndex + ">" + options.closeSymbol + "</a>");
$this.after($clearButton);
$this.next().css({
color: options.color,
'text-decoration': 'none',
display: 'none',
'line-height': 1,
overflow: 'hidden',
position: 'absolute',
right: options.right,
top: options.top
}, this);
if (options.paddingRight) {
$this.css({
'padding-right': options.paddingRight
});
}
if ($this.val().length >= 1 && options.showOnLoad === true) {
$clearButton.css({display: 'block'});
}
$this.focus(function() {
if ($(this).val().length >= 1) {
$clearButton.css({display: 'block'});
}
});
$this.change(function() {
if ($(this).val().length >= 1) {
$clearButton.css({display: 'block'});
}
});
$this.blur(function(e) {
if (options.hideOnBlur) {
setTimeout(function() {
var relatedTarget = e.relatedTarget || e.explicitOriginalTarget || document.activeElement;
if (relatedTarget !== $clearButton[0]) {
$clearButton.css({display: 'none'});
}
}, 0);
}
});
var handleUserInput = function() {
if ($(this).val().length >= 1) {
$clearButton.css({display: 'block'});
} else {
$clearButton.css({display: 'none'});
}
};
var handleInput = function () {
$this.off('keyup', handleUserInput);
$this.off('cut', handleUserInput);
handleInput = handleUserInput;
handleUserInput.call(this);
};
$this.on('keyup', handleUserInput);
$this.on('cut', function () {
var self = this;
setTimeout(function () {
handleUserInput.call(self);
}, 0);
});
$this.on('input', function () {
handleInput.call(this);
});
if (options.hideOnBlur) {
$clearButton.blur(function () {
$clearButton.css({display: 'none'});
});
}
$clearButton.click(function(e) {
var $input = $(me.element);
$input.val("");
$(this).css({display: 'none'});
if (options.returnFocus === true) {
$input.focus();
} else {
$input.trigger('change');
}
if (options.onClear) {
options.onClear($input);
}
e.preventDefault();
});
}
};
$.fn[pluginName] = function(options) {
return this.each(function() {
if (!$.data(this, "plugin_" + pluginName)) {
$.data(this, "plugin_" + pluginName,
new Plugin(this, options));
}
});
};
})(jQuery, window, document);

File diff suppressed because it is too large Load Diff

11
bijoe/static/js/jquery.form.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,13 +1,14 @@
{% extends "bijoe/base.html" %}
{% load i18n staticfiles django_select2_tags %}
{% load i18n staticfiles bijoe %}
{% 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>
{{ form.media.css }}
{{ form.media.js }}
<style>
form p {
margin-bottom: 0.5ex;
@ -30,7 +31,7 @@ form h3 {
padding: 0px;
text-align: justify;
display: flex;
justify-content: space-around;
justify-content: space-around;
}
#ods {
margin: 1ex;
@ -73,29 +74,6 @@ tbody tr:nth-child(even) td:not([rowspan]) {
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;
table-layout: fixed;
}
table.bijoe-table tr {
display: table;
table-layout: fixed;
width: 100%;
}
table.bijoe-table tbody {
flex: 1 1 auto;
display: block;
overflow-y: auto;
} */
input[type=submit] {
float: right;
z-index: 1;
@ -106,6 +84,42 @@ th {
text-overflow: ellipsis;
text-align: left;
}
.download {
float: right;
bottom: -3em;
left: -2em;
position: relative;
background: #aaaaaa linear-gradient(to bottom, #f9f9f9, #eeeeee) repeat scroll 0 0;
border: 1px solid #b7b7b7;
border-radius: 1px;
box-shadow: 0 2px 2px 0 #ddd;
color: #424258;
cursor: pointer;
line-height: 100%;
padding: 1ex 2ex;
text-decoration: none;
}
</style>
<script>
function human_join(a) {
if (a.length == 0) {
return '';
}else if (a.length == 1) {
return a[0].toString();
} else {
return a.slice(0, -1).join(', ') + ' et ' + a.slice(-1)[0];
}
}
</script>
<style type="text/css" media="print">
/* hide UI */
#top, #header, #more-user-links, .download, #content form {
display: none;
}
/* resize content */
#data {
width: 100%;
}
</style>
{% endblock %}
@ -151,7 +165,7 @@ th {
{% endif %}
</p>
{% endwith %}
<h3>Filtre(s)</h3
<h3>Filtre(s)</h3>
{% for field in form %}
{% 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 %}>
@ -173,6 +187,7 @@ th {
</form>
<div id="data">
{% if data %}
<a href="?{% firstof view.request.POST.urlencode view.request.GET.urlencode %}">URL</a>
{% if representation == 'table' %}
<table class="bijoe-table">
<thead>
@ -195,14 +210,39 @@ th {
</table>
{% else %}
{% for measure in measures %}
<h2>{{ measure.label }}</h2>
<a href="#" download="graph.png" class="download">PNG</a>
<canvas style="width: 100%" id="canvas-{{ measure.name }}"></canvas>
<a href="#" download="graph.png" class="download">Download</a>
<script>
console.log('coucou');
function wrap_text(s, width) {
var words = s.split(" ");
var s = "";
var l = 0;
for (var i = 0; i < words.length; i++) {
l += 1 + words[i].length;
if (l > width) {
s = s + "\n";
l = words[i].length;
} else {
s = s + " ";
}
s = s + words[i];
}
return s;
}
var Colors = {};
Colors.spaced_hsla = function (i, n, s, l, a) {
var h = 360 * i/n;
return "hsla(" + h.toString() + ', ' + s.toString() + '%, ' + l.toString() + '%, ' + a.toString() + ')';
};
var data = {{ json|safe }};
var dataset = {data: [], label: "{{ measure.label }}", axis: 1}
var dataset = {data: [], title: "{{ measure.label }}", fillColor: [], strokeColor: [],
highlightFill: [], highlightStroke: []}
var linedata = {labels: [], datasets: [dataset]};
var dimensions = {{ drilldown_json|safe }};
var dimension_labels = [];
for (var i = 0; i < dimensions.length; i++) {
dimension_labels.push(dimensions[i].label);
}
for (var i = 0; i < data.length; i++) {
var row = data[i];
var label = [];
@ -210,20 +250,96 @@ for (var i = 0; i < data.length; i++) {
for (var j = 0; j < row.coords.length; j++) {
label.push(row.coords[j].value)
}
label = label.join(' ');
label = label.join(', ');
linedata.labels.push(label)
var n = data.length;
for (var j = 0; j < row.measures.length; j++) {
if (row.measures[j].label != "{{ measure.label }}") {
continue;
}
dataset.data.push(row.measures[j].value);
dataset.fillColor.push(Colors.spaced_hsla(i, n, 100, 30, 0.5));
dataset.strokeColor.push(Colors.spaced_hsla(i, n, 100, 30, 0.75));
dataset.highlightFill.push(Colors.spaced_hsla(i, n, 100, 30, 0.75));
dataset.highlightStroke.push(Colors.spaced_hsla(i, n, 100, 30, 1));
}
}
console.log(linedata);
var ctx = $('#canvas-{{ measure.name }}')[0].getContext("2d");
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, $('canvas')[0].width, $('canvas')[0].height);
console.log("linedata", linedata);
var canvas = $('#canvas-{{ measure.name }}')[0];
var ctx = canvas.getContext("2d");
var option = {
animationSteps: 10,
//String - A legend template
responsive: true,
responseScaleContent: true,
legend: true,
dynamicDisplay: false,
yAxisMinimumInterval: 10,
inGraphDataShow: true,
legendFontColor: "rgb(0,0,0,1)",
scaleFontColor: "rgb(0,0,0,1)",
roundNumber: -2,
annotateDisplay : true,
datasetFill : true,
scaleLabel: "<%=value%>",
scaleFontSize : 16,
canvasBorders : true,
graphTitle : wrap_text("{{ measure.label|capfirst }}" + " par " + human_join(dimension_labels), 60),
graphTitleFontFamily : "'Arial'",
graphTitleFontSize : 24,
graphTitleFontStyle : "bold",
graphTitleFontColor : "#666",
legend : false,
}
if ("{{ measure.name }}".indexOf("delay") != -1) {
option.yAxisLabel = "jours";
}
if ("{{ measure.name }}" == "count") {
option.yAxisLabel = "quantité";
}
if ("{{ measure.name }}" == "percent") {
option.yAxisLabel = "pourcentage";
}
if ("{{ measure.name }}" == "percent") {
// pivot
var labels = linedata.labels;
linedata.labels = [""];
var datasets = [];
var empty = []
var Colors = {};
Colors.spaced_hsla = function (i, n, s, l, a) {
var h = 360 * i/n;
return "hsla(" + h.toString() + ', ' + s.toString() + '%, ' + l.toString() + '%, ' + a.toString() + ')';
}
var n = linedata.datasets[0].data.length;
for (var i = 0; i < n; i++) {
var value = linedata.datasets[0].data[i];
if (value > 1) {
dataset = {data: [value], title: labels[i] || "Aucun(e)"};
$.extend(dataset, {
fillColor: Colors.spaced_hsla(i, n, 100, 30, 0.5),
strokeColor: Colors.spaced_hsla(i, n, 100, 30, 0.75),
highlightFill: Colors.spaced_hsla(i, n, 100, 30, 0.75),
highlightStroke: Colors.spaced_hsla(i, n, 100, 30, 1)
})
datasets.push(dataset);
} else {
empty.push(labels[i]);
}
}
option.inGraphDataTmpl = "<%=v1+' '+v6+' %'%>";
option.annotateLabel = "<%=v1+' '+v6+' %'%>";
datasets.sort(function (a, b) { return a.value - b.value; });
if (empty.length) {
option.footNote = wrap_text("Les valeurs pour " + human_join(empty) + " sont nulles ou presque.", 110);
}
linedata.datasets = datasets;
console.log("pie data", linedata);
new Chart(ctx).Pie(linedata, option);
} else {
$.extend(option, {
//Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
scaleBeginAtZero : true,
@ -243,7 +359,7 @@ ctx.fillRect(0, 0, $('canvas')[0].width, $('canvas')[0].height);
scaleShowVerticalLines: true,
//Boolean - If there is a stroke on each bar
barShowStroke : true,
// barShowStroke : true,
//Number - Pixel width of the bar stroke
barStrokeWidth : 2,
@ -253,39 +369,21 @@ ctx.fillRect(0, 0, $('canvas')[0].width, $('canvas')[0].height);
//Number - Spacing between data sets within X values
barDatasetSpacing : 1,
//String - A legend template
responsive: true,
responsiveMinHeight: 300,
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";
});
new Chart(ctx).Bar(linedata, option);
}
if ("{{ measure.name }}" == "count") {
option.yAxisLabel = "quantité";
}
if ("{{ measure.name }}" == "percent") {
option.yAxisLabel = "pourcentage";
}
new Chart(ctx).Bar(linedata, option);
console.log("option", 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");
@ -295,8 +393,7 @@ function toDataURL(canvas) {
}
$(".download").on('click', function() {
this.href = toDataURL($(this).prev("canvas")[0]);
console.log(this.href);
this.href = toDataURL($(this).next("canvas")[0]);
})
</script>
{% endfor %}
@ -317,6 +414,7 @@ $(function () {
setTimeout(function () {
$('form').ajaxSubmit({success: function (data) {
var $content = $(data);
$('#data').empty();
$('#data').replaceWith($content.find('#data'));
}});
}, 800);
@ -328,7 +426,6 @@ $(function () {
{% endblock %}
{% block page-end %}
{{ form.media.js }}
<script>
{% if data %}
{% endif %}

View File

View File

@ -0,0 +1,11 @@
from django import template
register = template.Library()
try:
from django_select2.templatetags.django_select2_tags import *
except ImportError:
@register.simple_tag(name='import_django_select2_js_css')
def import_all(light=0):
return ''

View File

@ -19,7 +19,6 @@ import datetime
import decimal
import urllib
from collections import OrderedDict
import logging
import hashlib
from django.shortcuts import resolve_url
@ -170,6 +169,10 @@ class CubeView(AuthorizationMixin, CubeMixin, FormView):
def get_form_kwargs(self):
kwargs = super(CubeView, self).get_form_kwargs()
kwargs['cube'] = self.cube
if self.request.method == 'GET' and self.request.GET:
kwargs.update({
'data': self.request.GET,
})
return kwargs
def form_valid(self, form):
@ -205,9 +208,12 @@ class CubeView(AuthorizationMixin, CubeMixin, FormView):
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['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']]
ctx['drilldown_json'] = json.dumps([
{'name': dim.name, 'label': dim.label} for dim in ctx['drilldown']])
json_data = []
for row in self.get_data(form.cleaned_data, stringify=False):
coords = []