0001-dataviz-add-cells-to-display-data-from-cubes-using-b.patch
combo/apps/dataviz/README | ||
---|---|---|
1 |
Data visualization cells |
|
2 |
======================== |
|
3 | ||
4 |
Gauge cell |
|
5 |
---------- |
|
6 | ||
7 |
FIXME |
|
8 | ||
9 |
Cubes cells |
|
10 |
----------- |
|
11 | ||
12 |
Those cells are activated by setting the CUBES_URL setting to the root URL of a |
|
13 |
Cubes[1] 1.1 server. |
|
14 | ||
15 |
Cubes server is accessed using the requests library, you can define custom |
|
16 |
parameters for the requests.get() calls by setting CUBES_REQUESTS_PARAMS, for |
|
17 |
example to disable SSL certificate validation: |
|
18 | ||
19 |
CUBES_REQUESTS_PARAMS = {'verify': False} |
|
20 | ||
21 |
The CubesBarChart cell use the Chart.js library (through the XStatic-Chart.js |
|
22 |
pacakge) to render bar charts of the selected aggregated data. The y axis |
|
23 |
measure the aggregate which is a computed, the x axis is the dimension chosen |
|
24 |
for the first drill-down axis. The second drilldown axis will be used to |
|
25 |
generate multiple datasets, one by dimension point, i.e. result generated for |
|
26 |
the second axis will be grouped along the first drilldown axis. |
|
27 | ||
28 |
Ordering by drilldown axis is automatically done using implicit ordering defined |
|
29 |
by the Cubes model. |
|
30 | ||
31 |
The CubesTable render the same data as CubesBarChart but by using HTML tables. |
|
32 |
The first drilldown axis is used for the row headers and the second drilldown |
|
33 |
axis for the column headers. By using the two axis at the same time you can make |
|
34 |
pivot tables. |
|
35 | ||
36 |
[1]: https://pythonhosted.org/cubes/ |
combo/apps/dataviz/__init__.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import re |
|
18 | ||
17 | 19 |
import django.apps |
20 |
from django.core import checks |
|
21 |
from django.conf import settings |
|
22 | ||
18 | 23 | |
19 | 24 |
class AppConfig(django.apps.AppConfig): |
20 | 25 |
name = 'combo.apps.dataviz' |
... | ... | |
23 | 28 |
from . import urls |
24 | 29 |
return urls.urlpatterns |
25 | 30 | |
31 |
def ready(self): |
|
32 |
@checks.register('settings') |
|
33 |
def check_settings(**kwargs): |
|
34 |
# Check if CUBES_URL is a proper URL string |
|
35 |
if (getattr(settings, 'CUBES_URL', None) is not None |
|
36 |
and (not isinstance(settings.CUBES_URL, str) |
|
37 |
or not re.match(r'https?://', settings.CUBES_URL))): |
|
38 |
yield checks.Error('settings.CUBES_URL must be an HTTP URL') |
|
39 | ||
26 | 40 |
default_app_config = 'combo.apps.dataviz.AppConfig' |
combo/apps/dataviz/forms.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2015 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import json |
|
18 | ||
19 |
from django.utils.translation import ugettext_lazy as _ |
|
20 |
from django import forms |
|
21 |
from django.conf import settings |
|
22 |
from django.core.exceptions import ValidationError |
|
23 | ||
24 |
from .models import CubesBarChart |
|
25 |
from .utils import get_cubes, get_cube, get_drilldown |
|
26 | ||
27 | ||
28 |
class CubesBarChartForm(forms.ModelForm): |
|
29 |
EMPTY = [(u'', _('None'))] |
|
30 | ||
31 |
class Meta: |
|
32 |
model = CubesBarChart |
|
33 |
fields = ('title', 'url', 'cube', 'aggregate1', 'drilldown1', 'drilldown2', |
|
34 |
'other_parameters') |
|
35 | ||
36 |
def __init__(self, *args, **kwargs): |
|
37 |
super(CubesBarChartForm, self).__init__(*args, **kwargs) |
|
38 |
for field in ('cube', 'aggregate1', 'drilldown1', 'drilldown2'): |
|
39 |
self.fields[field] = forms.ChoiceField( |
|
40 |
label=self.fields[field].label, |
|
41 |
initial=self.fields[field].initial, |
|
42 |
required=False, |
|
43 |
choices=self.EMPTY) |
|
44 |
if getattr(settings, 'CUBES_URL', None): |
|
45 |
cube_choices = self.get_cubes_choices() |
|
46 |
if cube_choices: |
|
47 |
self.fields['cube'].choices = cube_choices |
|
48 |
aggregate1_choices = self.get_aggregate_choices() |
|
49 |
if aggregate1_choices: |
|
50 |
self.fields['aggregate1'].choices = aggregate1_choices |
|
51 |
drilldown_choices = self.get_drilldown_choices() |
|
52 |
if drilldown_choices: |
|
53 |
self.fields['drilldown1'].choices = drilldown_choices |
|
54 |
self.fields['drilldown2'].choices = drilldown_choices |
|
55 | ||
56 |
def clean_other_parameters(self): |
|
57 |
other_parameters = self.cleaned_data['other_parameters'] |
|
58 |
if other_parameters: |
|
59 |
try: |
|
60 |
decoded = json.loads(other_parameters) |
|
61 |
assert isinstance(decoded, dict) |
|
62 |
for key, value in decoded.iteritems(): |
|
63 |
assert isinstance(key, unicode) |
|
64 |
assert isinstance(value, unicode) |
|
65 |
except (ValueError, AssertionError): |
|
66 |
raise ValidationError(_('Other parameter must be a JSON object containing only ' |
|
67 |
'strings')) |
|
68 |
return other_parameters |
|
69 | ||
70 |
def get_cubes_choices(self): |
|
71 |
cubes = get_cubes() |
|
72 |
return self.EMPTY + [(cube['name'], cube.get('label')) for cube in cubes] |
|
73 | ||
74 |
def get_aggregate_choices(self): |
|
75 |
cube = self.instance.cube |
|
76 |
if cube: |
|
77 |
cube = get_cube(cube) |
|
78 |
if cube: |
|
79 |
return self.EMPTY + [(ag['name'], ag['label']) for ag in cube.get('aggregates', [])] |
|
80 |
return [] |
|
81 | ||
82 |
def get_drilldown_choices(self): |
|
83 |
cube = self.instance.cube |
|
84 |
if cube: |
|
85 |
choices = get_drilldown(cube) |
|
86 |
if choices: |
|
87 |
return self.EMPTY + choices |
|
88 |
return [] |
combo/apps/dataviz/migrations/0003_cubesbarchart_cubestable.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import models, migrations |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('auth', '0001_initial'), |
|
11 |
('data', '0012_auto_20151029_1535'), |
|
12 |
('dataviz', '0002_gauge_jsonp_data_source'), |
|
13 |
] |
|
14 | ||
15 |
operations = [ |
|
16 |
migrations.CreateModel( |
|
17 |
name='CubesBarChart', |
|
18 |
fields=[ |
|
19 |
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
|
20 |
('placeholder', models.CharField(max_length=20)), |
|
21 |
('order', models.PositiveIntegerField()), |
|
22 |
('slug', models.SlugField(verbose_name='Slug', blank=True)), |
|
23 |
('public', models.BooleanField(default=True, verbose_name='Public')), |
|
24 |
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')), |
|
25 |
('title', models.CharField(max_length=150, null=True, verbose_name='Title', blank=True)), |
|
26 |
('url', models.URLField(max_length=150, null=True, verbose_name='URL', blank=True)), |
|
27 |
('cube', models.CharField(max_length=256, null=True, verbose_name='Cube', blank=True)), |
|
28 |
('aggregate1', models.CharField(max_length=64, null=True, verbose_name='Aggregate', blank=True)), |
|
29 |
('drilldown1', models.CharField(max_length=64, null=True, verbose_name='Drilldown 1', blank=True)), |
|
30 |
('drilldown2', models.CharField(max_length=64, null=True, verbose_name='Drilldown 2', blank=True)), |
|
31 |
('other_parameters', models.TextField(null=True, verbose_name='Other parameters', blank=True)), |
|
32 |
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)), |
|
33 |
('page', models.ForeignKey(to='data.Page')), |
|
34 |
], |
|
35 |
options={ |
|
36 |
'verbose_name': 'Cubes Barchart', |
|
37 |
}, |
|
38 |
bases=(models.Model,), |
|
39 |
), |
|
40 |
migrations.CreateModel( |
|
41 |
name='CubesTable', |
|
42 |
fields=[ |
|
43 |
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
|
44 |
('placeholder', models.CharField(max_length=20)), |
|
45 |
('order', models.PositiveIntegerField()), |
|
46 |
('slug', models.SlugField(verbose_name='Slug', blank=True)), |
|
47 |
('public', models.BooleanField(default=True, verbose_name='Public')), |
|
48 |
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')), |
|
49 |
('title', models.CharField(max_length=150, null=True, verbose_name='Title', blank=True)), |
|
50 |
('url', models.URLField(max_length=150, null=True, verbose_name='URL', blank=True)), |
|
51 |
('cube', models.CharField(max_length=256, null=True, verbose_name='Cube', blank=True)), |
|
52 |
('aggregate1', models.CharField(max_length=64, null=True, verbose_name='Aggregate', blank=True)), |
|
53 |
('drilldown1', models.CharField(max_length=64, null=True, verbose_name='Drilldown 1', blank=True)), |
|
54 |
('drilldown2', models.CharField(max_length=64, null=True, verbose_name='Drilldown 2', blank=True)), |
|
55 |
('other_parameters', models.TextField(null=True, verbose_name='Other parameters', blank=True)), |
|
56 |
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)), |
|
57 |
('page', models.ForeignKey(to='data.Page')), |
|
58 |
], |
|
59 |
options={ |
|
60 |
'verbose_name': 'Cubes Table', |
|
61 |
}, |
|
62 |
bases=(models.Model,), |
|
63 |
), |
|
64 |
] |
combo/apps/dataviz/models.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import json |
|
18 |
from collections import OrderedDict |
|
19 | ||
17 | 20 |
from django.core.urlresolvers import reverse |
18 | 21 |
from django.db import models |
19 | 22 |
from django.utils.translation import ugettext_lazy as _ |
23 |
from django.conf import settings |
|
20 | 24 | |
21 | 25 |
from combo.data.models import CellBase |
22 | 26 |
from combo.data.library import register_cell_class |
... | ... | |
26 | 30 |
class Gauge(CellBase): |
27 | 31 |
title = models.CharField(_('Title'), max_length=150, blank=True, null=True) |
28 | 32 |
url = models.URLField(_('URL'), max_length=150, blank=True, null=True) |
29 |
data_source = models.CharField(_('Data Source'), max_length=150, |
|
30 |
blank=True, null=True) |
|
31 |
jsonp_data_source = models.BooleanField(_('Use JSONP to get data'), |
|
32 |
default=True) |
|
33 |
data_source = models.CharField(_('Data Source'), max_length=150, blank=True, null=True) |
|
34 |
jsonp_data_source = models.BooleanField(_('Use JSONP to get data'), default=True) |
|
33 | 35 |
max_value = models.PositiveIntegerField(_('Max Value'), blank=True, null=True) |
34 | 36 | |
35 | 37 |
template_name = 'combo/gauge-cell.html' |
... | ... | |
55 | 57 |
'data_source_url': data_source_url, |
56 | 58 |
'jsonp': self.jsonp_data_source, |
57 | 59 |
} |
60 | ||
61 | ||
62 |
class BaseCubesChart(CellBase): |
|
63 |
title = models.CharField(_('Title'), max_length=150, blank=True, null=True) |
|
64 |
url = models.URLField(_('URL'), max_length=150, blank=True, null=True) |
|
65 |
cube = models.CharField(verbose_name=_('Cube'), max_length=256, blank=True, null=True) |
|
66 |
aggregate1 = models.CharField(verbose_name=_('Aggregate'), max_length=64, blank=True, null=True) |
|
67 |
drilldown1 = models.CharField(verbose_name=_('Drilldown 1'), max_length=64, blank=True, |
|
68 |
null=True) |
|
69 |
drilldown2 = models.CharField(verbose_name=_('Drilldown 2'), max_length=64, blank=True, |
|
70 |
null=True) |
|
71 |
other_parameters = models.TextField(verbose_name=_('Other parameters'), blank=True, null=True) |
|
72 | ||
73 |
class Meta: |
|
74 |
abstract = True |
|
75 | ||
76 |
@classmethod |
|
77 |
def is_enabled(self): |
|
78 |
return bool(getattr(settings, 'CUBES_URL', None)) |
|
79 | ||
80 |
def get_additional_label(self): |
|
81 |
return self.title |
|
82 | ||
83 |
def get_default_form_class(self): |
|
84 |
from .forms import CubesBarChartForm |
|
85 |
return CubesBarChartForm |
|
86 | ||
87 |
def get_cell_extra_context(self): |
|
88 |
return { |
|
89 |
'cell': self, |
|
90 |
'title': self.title, |
|
91 |
'url': self.url, |
|
92 |
'aggregate': self.get_aggregate(), |
|
93 |
} |
|
94 | ||
95 |
def get_aggregate(self): |
|
96 |
'''Get aggregate defined by chosen cube and the two drildown paths, request ordering of the |
|
97 |
data by natural order of each axis.''' |
|
98 |
from .utils import get_aggregate, get_cube, compute_levels |
|
99 |
aggregate = get_aggregate(name=self.cube, |
|
100 |
aggregate1=self.aggregate1, |
|
101 |
drilldown1=self.drilldown1, |
|
102 |
drilldown2=self.drilldown2, |
|
103 |
other_parameters=(json.loads(self.other_parameters) if |
|
104 |
self.other_parameters else None)) |
|
105 |
cube = get_cube(self.cube) |
|
106 |
if not aggregate or not cube: |
|
107 |
return |
|
108 | ||
109 |
label_refs1 = [] |
|
110 |
key_refs1 = [] |
|
111 |
if self.drilldown1: |
|
112 |
compute_levels(cube, self.drilldown1, label_refs=label_refs1, key_refs=key_refs1) |
|
113 |
key_refs2 = [] |
|
114 |
label_refs2 = [] |
|
115 |
if self.drilldown2: |
|
116 |
compute_levels(cube, self.drilldown2, label_refs=label_refs2, key_refs=key_refs2) |
|
117 |
for ag in cube['aggregates']: |
|
118 |
if ag['name'] != self.aggregate1: |
|
119 |
continue |
|
120 |
break |
|
121 | ||
122 |
def cell_ref(cell, refs): |
|
123 |
return tuple(cell[ref] for ref in refs) |
|
124 | ||
125 |
keys1 = OrderedDict() |
|
126 |
labels = OrderedDict() |
|
127 |
datasets = OrderedDict() |
|
128 | ||
129 |
for cell in aggregate['cells']: |
|
130 |
label1 = u' / '.join(map(unicode, cell_ref(cell, label_refs1))) |
|
131 |
key1 = cell_ref(cell, key_refs1) |
|
132 |
labels[key1] = label1 |
|
133 |
keys1[key1] = 1 |
|
134 |
if key_refs2: |
|
135 |
label2 = u' / '.join(map(unicode, cell_ref(cell, label_refs2))) |
|
136 |
key2 = cell_ref(cell, key_refs2) |
|
137 |
else: |
|
138 |
label2 = '' |
|
139 |
key2 = 1 |
|
140 |
dataset = datasets.setdefault(key2, {'label': label2, |
|
141 |
'data': OrderedDict()}) |
|
142 |
value = cell[self.aggregate1] |
|
143 |
dataset['data'][key1] = value |
|
144 |
for dataset in datasets.itervalues(): |
|
145 |
dataset['data'] = [dataset['data'].get(key, 0) for key in keys1] |
|
146 | ||
147 |
return { |
|
148 |
'labels': labels.values(), |
|
149 |
'datasets': [{ |
|
150 |
'label': dataset['label'], |
|
151 |
'data': dataset['data'], |
|
152 |
} for dataset in datasets.itervalues()] |
|
153 |
} |
|
154 | ||
155 | ||
156 |
@register_cell_class |
|
157 |
class CubesBarChart(BaseCubesChart): |
|
158 |
template_name = 'combo/cubes-barchart.html' |
|
159 | ||
160 |
class Media: |
|
161 |
js = ('xstatic/Chart.min.js', 'js/combo.cubes-barchart.js') |
|
162 | ||
163 |
class Meta: |
|
164 |
verbose_name = _('Cubes Barchart') |
|
165 | ||
166 |
def get_cell_extra_context(self): |
|
167 |
ctx = super(CubesBarChart, self).get_cell_extra_context() |
|
168 |
# Need JSON serialization to pass data to Chart.js |
|
169 |
ctx['json_aggregate'] = json.dumps(ctx['aggregate']) |
|
170 |
return ctx |
|
171 | ||
172 | ||
173 |
@register_cell_class |
|
174 |
class CubesTable(BaseCubesChart): |
|
175 |
template_name = 'combo/cubes-table.html' |
|
176 | ||
177 |
class Meta: |
|
178 |
verbose_name = _('Cubes Table') |
combo/apps/dataviz/static/js/combo.cubes-barchart.js | ||
---|---|---|
1 |
$(function() { |
|
2 |
var Colors = {}; |
|
3 | ||
4 |
Colors.spaced_hsla = function (i, n, s, l, a) { |
|
5 |
var h = 360 * i/n; |
|
6 |
return "hsla(" + h.toString() + ', ' + s.toString() + '%, ' + l.toString() + '%, ' + a.toString() + ')'; |
|
7 |
} |
|
8 | ||
9 |
$('.combo-cube-aggregate').each(function(idx, elem) { |
|
10 |
var cube_url = $(elem).data('cube-url'); |
|
11 |
var aggregate_url = $(elem).data('aggregate-url'); |
|
12 |
var model = null; |
|
13 |
var ctx = $('canvas', elem)[0].getContext("2d"); |
|
14 |
var id = $(elem).data('combo-cube-aggregate-id'); |
|
15 | ||
16 |
var option = { |
|
17 |
//Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value |
|
18 |
scaleBeginAtZero : true, |
|
19 | ||
20 |
//Boolean - Whether grid lines are shown across the chart |
|
21 |
scaleShowGridLines : true, |
|
22 | ||
23 |
//String - Colour of the grid lines |
|
24 |
scaleGridLineColor : "rgba(0,0,0,.05)", |
|
25 | ||
26 |
//Number - Width of the grid lines |
|
27 |
scaleGridLineWidth : 1, |
|
28 | ||
29 |
//Boolean - Whether to show horizontal lines (except X axis) |
|
30 |
scaleShowHorizontalLines: true, |
|
31 | ||
32 |
//Boolean - Whether to show vertical lines (except Y axis) |
|
33 |
scaleShowVerticalLines: true, |
|
34 | ||
35 |
//Boolean - If there is a stroke on each bar |
|
36 |
barShowStroke : true, |
|
37 | ||
38 |
//Number - Pixel width of the bar stroke |
|
39 |
barStrokeWidth : 2, |
|
40 | ||
41 |
//Number - Spacing between each of the X value sets |
|
42 |
barValueSpacing : 5, |
|
43 | ||
44 |
//Number - Spacing between data sets within X values |
|
45 |
barDatasetSpacing : 1, |
|
46 | ||
47 |
//String - A legend template |
|
48 |
legendTemplate : "ul", |
|
49 |
multiTooltipTemplate: "<%= datasetLabel %>: <%= value %>", |
|
50 |
responsive: true, |
|
51 |
} |
|
52 |
var data = window['combo_cube_aggregate_' + id]; |
|
53 |
// Set one color by dataset |
|
54 |
var n = data.datasets.length; |
|
55 |
for (var i = 0; i < n; i++) { |
|
56 |
var dataset = data.datasets[i]; |
|
57 |
$.extend(dataset, { |
|
58 |
fillColor: Colors.spaced_hsla(i, n, 100, 30, 0.5), |
|
59 |
strokeColor: Colors.spaced_hsla(i, n, 100, 30, 0.75), |
|
60 |
highlightFill: Colors.spaced_hsla(i, n, 100, 30, 0.75), |
|
61 |
highlightStroke: Colors.spaced_hsla(i, n, 100, 30, 1) |
|
62 |
}) |
|
63 |
} |
|
64 |
var clone = function(obj){ |
|
65 |
var objClone = {}; |
|
66 |
for (var key in obj) { |
|
67 |
if (obj.hasOwnProperty(key)) objClone[key] = obj[key]; |
|
68 |
}; |
|
69 |
return objClone; |
|
70 |
} |
|
71 |
var chart = new Chart(ctx).Bar(data, option); |
|
72 |
if (chart.datasets.length == 1) { |
|
73 |
// Set one color by bar |
|
74 |
var n = chart.datasets[0].bars.length; |
|
75 |
for (var i = 0; i < n; i++) { |
|
76 |
var bar = chart.datasets[0].bars[i]; |
|
77 |
$.extend(bar, { |
|
78 |
fillColor: Colors.spaced_hsla(i, n, 100, 30, 0.5), |
|
79 |
strokeColor: Colors.spaced_hsla(i, n, 100, 30, 0.75), |
|
80 |
highlightFill: Colors.spaced_hsla(i, n, 100, 30, 0.75), |
|
81 |
highlightStroke: Colors.spaced_hsla(i, n, 100, 30, 1) |
|
82 |
}) |
|
83 |
bar['_saved'] = clone(bar); |
|
84 |
bar.update(); |
|
85 |
} |
|
86 |
} |
|
87 |
window.chart = chart; |
|
88 |
}) |
|
89 |
}) |
combo/apps/dataviz/templates/combo/cubes-barchart.html | ||
---|---|---|
1 |
<script> |
|
2 |
var combo_cube_aggregate_{{ cell.id }} = {{ json_aggregate|safe }}; |
|
3 |
</script> |
|
4 |
<div |
|
5 |
class="combo-cube-aggregate" |
|
6 |
data-combo-cube-aggregate-id="{{ cell.id }}"> |
|
7 |
{% if title %} |
|
8 |
{% if url %}<a href="{{url}}">{% endif %}{{title}}{% if url %}</a>{% endif %} |
|
9 |
{% endif %} |
|
10 |
<canvas style="width: 100%;"> |
|
11 |
</canvas> |
|
12 |
</div> |
combo/apps/dataviz/templates/combo/cubes-table.html | ||
---|---|---|
1 |
<table class="combo-cube-table"> |
|
2 |
{% if title %} |
|
3 |
<caption> |
|
4 |
{% if url %}<a href="{{url}}">{% endif %}{{title}}{% if url %}</a>{% endif %} |
|
5 |
</caption> |
|
6 |
{% endif %} |
|
7 |
<thead> |
|
8 |
<th></th> |
|
9 |
{% for label in aggregate.labels %} |
|
10 |
<td>{{ label }}</td> |
|
11 |
{% endfor %} |
|
12 |
</thead> |
|
13 |
<tbody> |
|
14 |
{% for dataset in aggregate.datasets %} |
|
15 |
<tr> |
|
16 |
<th>{{ dataset.label }}</th> |
|
17 |
{% for value in dataset.data %} |
|
18 |
<td>{{ value }}</td> |
|
19 |
{% endfor %} |
|
20 |
</tr> |
|
21 |
{% endfor %} |
|
22 |
</tbody> |
|
23 |
</table> |
combo/apps/dataviz/urls.py | ||
---|---|---|
18 | 18 | |
19 | 19 |
from .views import ajax_gauge_count |
20 | 20 | |
21 |
urlpatterns = patterns('', |
|
21 |
urlpatterns = patterns( |
|
22 |
'', |
|
22 | 23 |
url(r'^ajax/gauge-count/(?P<cell>[\w_-]+)/$', |
23 | 24 |
ajax_gauge_count, name='combo-ajax-gauge-count'), |
24 | 25 |
) |
combo/apps/dataviz/utils.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2015 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import requests |
|
18 |
from requests.exceptions import RequestException |
|
19 |
import urlparse |
|
20 | ||
21 |
from django.conf import settings |
|
22 | ||
23 | ||
24 |
def get_requests_params(): |
|
25 |
return getattr(settings, 'CUBES_REQUEST_PARAMS', {}) |
|
26 | ||
27 | ||
28 |
def get_cubes(): |
|
29 |
try: |
|
30 |
r = requests.get(urlparse.urljoin(settings.CUBES_URL, 'cubes'), **get_requests_params) |
|
31 |
except RequestException: |
|
32 |
return [] |
|
33 |
try: |
|
34 |
return r.json() |
|
35 |
except ValueError: |
|
36 |
return [] |
|
37 | ||
38 | ||
39 |
def get_cube(name): |
|
40 |
model_url = urlparse.urljoin(settings.CUBES_URL, 'cube/%s/model' % name) |
|
41 |
try: |
|
42 |
r = requests.get(model_url, **get_requests_params) |
|
43 |
except RequestException: |
|
44 |
return None |
|
45 |
try: |
|
46 |
return r.json() |
|
47 |
except ValueError: |
|
48 |
return None |
|
49 | ||
50 | ||
51 |
def get_drilldown(name): |
|
52 |
cube = get_cube(name) |
|
53 |
if not cube: |
|
54 |
return [] |
|
55 |
l = [] |
|
56 |
for dimension in cube.get('dimensions', []): |
|
57 |
dim_name = dimension['name'] |
|
58 |
dim_label = dimension.get('label') or dim_name |
|
59 |
if dimension.get('levels'): |
|
60 |
levels = {} |
|
61 |
for level in dimension['levels']: |
|
62 |
levels[level['name']] = level.get('label') or level['name'] |
|
63 |
if dimension.get('hierarchies'): |
|
64 |
for hierarchy in dimension['hierarchies']: |
|
65 |
h_name = hierarchy['name'] |
|
66 |
h_label = hierarchy.get('label') or h_name |
|
67 |
if h_name == 'default': |
|
68 |
h_label = '' |
|
69 |
for level in hierarchy['levels']: |
|
70 |
labels = filter(None, [dim_label, h_label, levels[level]]) |
|
71 |
label = ' - '.join(labels) |
|
72 |
name = '%s@%s:%s' % (dim_name, h_name, level) |
|
73 |
l.append((name, label)) |
|
74 |
else: |
|
75 |
raise NotImplementedError |
|
76 |
else: |
|
77 |
l.append((dim_name, dim_label)) |
|
78 |
return l |
|
79 | ||
80 | ||
81 |
def compute_levels(cube, drilldown, key_refs=None, label_refs=None): |
|
82 |
from .utils import get_attribute_ref |
|
83 |
dim = drilldown.split('@')[0] |
|
84 |
hier = drilldown.split('@')[1].split(':')[0] |
|
85 |
lev = drilldown.split(':')[1] |
|
86 | ||
87 |
for dimension in cube['dimensions']: |
|
88 |
if dimension['name'] != dim: |
|
89 |
continue |
|
90 |
level_label_refs = {} |
|
91 |
level_key_refs = {} |
|
92 |
for level in dimension['levels']: |
|
93 |
level_key_refs[level['name']] = get_attribute_ref(level, level['key']) |
|
94 |
level_label_refs[level['name']] = get_attribute_ref(level, level['label_attribute']) |
|
95 |
for hierarchy in dimension['hierarchies']: |
|
96 |
if hierarchy['name'] != hier: |
|
97 |
continue |
|
98 |
for level in hierarchy['levels']: |
|
99 |
if key_refs is not None: |
|
100 |
key_refs.append(level_key_refs[level]) |
|
101 |
if label_refs is not None: |
|
102 |
label_refs.append(level_label_refs[level]) |
|
103 |
if level == lev: |
|
104 |
break |
|
105 |
break |
|
106 |
break |
|
107 | ||
108 | ||
109 |
def get_aggregate(name, aggregate1, drilldown1, drilldown2, other_parameters=None): |
|
110 |
if not name: |
|
111 |
return None |
|
112 |
cube = get_cube(name) |
|
113 |
aggregate_url = urlparse.urljoin(settings.CUBES_URL, 'cube/%s/aggregate' % name) |
|
114 |
if not aggregate1: |
|
115 |
return None |
|
116 |
try: |
|
117 |
params = {'aggregate': aggregate1} |
|
118 |
drilldowns = [] |
|
119 |
key_refs = [] |
|
120 |
if drilldown1: |
|
121 |
compute_levels(cube, drilldown1, key_refs=key_refs) |
|
122 |
drilldowns.append(drilldown1) |
|
123 |
if drilldown2: |
|
124 |
compute_levels(cube, drilldown2, key_refs=key_refs) |
|
125 |
drilldowns.append(drilldown2) |
|
126 |
if drilldowns: |
|
127 |
params['drilldown'] = drilldowns |
|
128 | ||
129 |
if key_refs: |
|
130 |
params['order'] = key_refs |
|
131 |
if other_parameters: |
|
132 |
params.update(other_parameters) |
|
133 | ||
134 |
r = requests.get(aggregate_url, params=params, **get_requests_params) |
|
135 |
except RequestException: |
|
136 |
return None |
|
137 |
try: |
|
138 |
return r.json() |
|
139 |
except ValueError: |
|
140 |
return None |
|
141 | ||
142 | ||
143 |
def get_attribute_ref(level, name): |
|
144 |
for attribute in level['attributes']: |
|
145 |
if attribute['name'] == name: |
|
146 |
return attribute['ref'] |
combo/apps/dataviz/views.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import json |
|
18 | 17 |
import requests |
19 | 18 | |
20 |
from django.conf import settings |
|
21 |
from django.http import Http404, HttpResponse |
|
19 |
from django.http import HttpResponse |
|
22 | 20 | |
23 | 21 |
from .models import Gauge |
24 | 22 |
combo/settings.py | ||
---|---|---|
63 | 63 |
'combo.apps.wcs', |
64 | 64 |
'combo.apps.publik', |
65 | 65 |
'combo.apps.family', |
66 |
'combo.apps.dataviz', |
|
67 |
'xstatic.pkg.chart_js', |
|
66 | 68 |
) |
67 | 69 | |
68 | 70 |
INSTALLED_APPS = plugins.register_plugins_apps(INSTALLED_APPS) |
setup.py | ||
---|---|---|
109 | 109 |
'feedparser', |
110 | 110 |
'django-jsonfield', |
111 | 111 |
'requests', |
112 |
'XStatic-Chart.js', |
|
112 | 113 |
], |
113 | 114 |
zip_safe=False, |
114 | 115 |
cmdclass={ |
115 |
- |