dataviz: add cells to display data from cubes using barcharts and HTML tables (#9098)
It contains two new cells: CubesBarChart and CubesTable. You must have a cubes server targeted by the CUBES_URL setting for enabling the cells.
This commit is contained in:
parent
85f0906aaf
commit
842f700b71
|
@ -0,0 +1,36 @@
|
||||||
|
Data visualization cells
|
||||||
|
========================
|
||||||
|
|
||||||
|
Gauge cell
|
||||||
|
----------
|
||||||
|
|
||||||
|
FIXME
|
||||||
|
|
||||||
|
Cubes cells
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Those cells are activated by setting the CUBES_URL setting to the root URL of a
|
||||||
|
Cubes[1] 1.1 server.
|
||||||
|
|
||||||
|
Cubes server is accessed using the requests library, you can define custom
|
||||||
|
parameters for the requests.get() calls by setting CUBES_REQUESTS_PARAMS, for
|
||||||
|
example to disable SSL certificate validation:
|
||||||
|
|
||||||
|
CUBES_REQUESTS_PARAMS = {'verify': False}
|
||||||
|
|
||||||
|
The CubesBarChart cell use the Chart.js library (through the XStatic-Chart.js
|
||||||
|
package) to render bar charts of the selected aggregated data. The y axis
|
||||||
|
measure the aggregate which is computed, the x axis is the dimension chosen
|
||||||
|
for the first drill-down axis. The second drilldown axis will be used to
|
||||||
|
generate multiple datasets, one by dimension point, i.e. results generated for
|
||||||
|
the second axis will be grouped along the first drilldown axis.
|
||||||
|
|
||||||
|
Ordering by drilldown axis is automatically done using implicit ordering defined
|
||||||
|
by the Cubes model.
|
||||||
|
|
||||||
|
The CubesTable render the same data as CubesBarChart but by using HTML tables.
|
||||||
|
The first drilldown axis is used for the row headers and the second drilldown
|
||||||
|
axis for the column headers. By using the two axis at the same time you can make
|
||||||
|
pivot tables.
|
||||||
|
|
||||||
|
[1]: https://pythonhosted.org/cubes/
|
|
@ -14,7 +14,12 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
import django.apps
|
import django.apps
|
||||||
|
from django.core import checks
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
class AppConfig(django.apps.AppConfig):
|
class AppConfig(django.apps.AppConfig):
|
||||||
name = 'combo.apps.dataviz'
|
name = 'combo.apps.dataviz'
|
||||||
|
@ -23,4 +28,13 @@ class AppConfig(django.apps.AppConfig):
|
||||||
from . import urls
|
from . import urls
|
||||||
return urls.urlpatterns
|
return urls.urlpatterns
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
@checks.register('settings')
|
||||||
|
def check_settings(**kwargs):
|
||||||
|
# Check if CUBES_URL is a proper URL string
|
||||||
|
if (getattr(settings, 'CUBES_URL', None) is not None
|
||||||
|
and (not isinstance(settings.CUBES_URL, str)
|
||||||
|
or not re.match(r'https?://', settings.CUBES_URL))):
|
||||||
|
yield checks.Error('settings.CUBES_URL must be an HTTP URL')
|
||||||
|
|
||||||
default_app_config = 'combo.apps.dataviz.AppConfig'
|
default_app_config = 'combo.apps.dataviz.AppConfig'
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
# combo - content management system
|
||||||
|
# Copyright (C) 2015 Entr'ouvert
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it
|
||||||
|
# under the terms of the GNU Affero General Public License as published
|
||||||
|
# by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
from .models import CubesBarChart
|
||||||
|
from .utils import get_cubes, get_cube, get_drilldown
|
||||||
|
|
||||||
|
|
||||||
|
class CubesBarChartForm(forms.ModelForm):
|
||||||
|
EMPTY = [(u'', _('None'))]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CubesBarChart
|
||||||
|
fields = ('title', 'url', 'cube', 'aggregate1', 'drilldown1', 'drilldown2',
|
||||||
|
'other_parameters')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(CubesBarChartForm, self).__init__(*args, **kwargs)
|
||||||
|
for field in ('cube', 'aggregate1', 'drilldown1', 'drilldown2'):
|
||||||
|
self.fields[field] = forms.ChoiceField(
|
||||||
|
label=self.fields[field].label,
|
||||||
|
initial=self.fields[field].initial,
|
||||||
|
required=False,
|
||||||
|
choices=self.EMPTY)
|
||||||
|
if getattr(settings, 'CUBES_URL', None):
|
||||||
|
cube_choices = self.get_cubes_choices()
|
||||||
|
if cube_choices:
|
||||||
|
self.fields['cube'].choices = cube_choices
|
||||||
|
aggregate1_choices = self.get_aggregate_choices()
|
||||||
|
if aggregate1_choices:
|
||||||
|
self.fields['aggregate1'].choices = aggregate1_choices
|
||||||
|
drilldown_choices = self.get_drilldown_choices()
|
||||||
|
if drilldown_choices:
|
||||||
|
self.fields['drilldown1'].choices = drilldown_choices
|
||||||
|
self.fields['drilldown2'].choices = drilldown_choices
|
||||||
|
|
||||||
|
def clean_other_parameters(self):
|
||||||
|
other_parameters = self.cleaned_data['other_parameters']
|
||||||
|
if other_parameters:
|
||||||
|
try:
|
||||||
|
decoded = json.loads(other_parameters)
|
||||||
|
assert isinstance(decoded, dict)
|
||||||
|
for key, value in decoded.iteritems():
|
||||||
|
assert isinstance(key, unicode)
|
||||||
|
assert isinstance(value, unicode)
|
||||||
|
except (ValueError, AssertionError):
|
||||||
|
raise ValidationError(_('Other parameter must be a JSON object containing only '
|
||||||
|
'strings'))
|
||||||
|
return other_parameters
|
||||||
|
|
||||||
|
def get_cubes_choices(self):
|
||||||
|
cubes = get_cubes()
|
||||||
|
return self.EMPTY + [(cube['name'], cube.get('label')) for cube in cubes]
|
||||||
|
|
||||||
|
def get_aggregate_choices(self):
|
||||||
|
cube = self.instance.cube
|
||||||
|
if cube:
|
||||||
|
cube = get_cube(cube)
|
||||||
|
if cube:
|
||||||
|
return self.EMPTY + [(ag['name'], ag['label']) for ag in cube.get('aggregates', [])]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_drilldown_choices(self):
|
||||||
|
cube = self.instance.cube
|
||||||
|
if cube:
|
||||||
|
choices = get_drilldown(cube)
|
||||||
|
if choices:
|
||||||
|
return self.EMPTY + choices
|
||||||
|
return []
|
|
@ -0,0 +1,64 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0001_initial'),
|
||||||
|
('data', '0012_auto_20151029_1535'),
|
||||||
|
('dataviz', '0002_gauge_jsonp_data_source'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CubesBarChart',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('placeholder', models.CharField(max_length=20)),
|
||||||
|
('order', models.PositiveIntegerField()),
|
||||||
|
('slug', models.SlugField(verbose_name='Slug', blank=True)),
|
||||||
|
('public', models.BooleanField(default=True, verbose_name='Public')),
|
||||||
|
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
|
||||||
|
('title', models.CharField(max_length=150, null=True, verbose_name='Title', blank=True)),
|
||||||
|
('url', models.URLField(max_length=150, null=True, verbose_name='URL', blank=True)),
|
||||||
|
('cube', models.CharField(max_length=256, null=True, verbose_name='Cube', blank=True)),
|
||||||
|
('aggregate1', models.CharField(max_length=64, null=True, verbose_name='Aggregate', blank=True)),
|
||||||
|
('drilldown1', models.CharField(max_length=64, null=True, verbose_name='Drilldown 1', blank=True)),
|
||||||
|
('drilldown2', models.CharField(max_length=64, null=True, verbose_name='Drilldown 2', blank=True)),
|
||||||
|
('other_parameters', models.TextField(null=True, verbose_name='Other parameters', blank=True)),
|
||||||
|
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
|
||||||
|
('page', models.ForeignKey(to='data.Page')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Cubes Barchart',
|
||||||
|
},
|
||||||
|
bases=(models.Model,),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CubesTable',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('placeholder', models.CharField(max_length=20)),
|
||||||
|
('order', models.PositiveIntegerField()),
|
||||||
|
('slug', models.SlugField(verbose_name='Slug', blank=True)),
|
||||||
|
('public', models.BooleanField(default=True, verbose_name='Public')),
|
||||||
|
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
|
||||||
|
('title', models.CharField(max_length=150, null=True, verbose_name='Title', blank=True)),
|
||||||
|
('url', models.URLField(max_length=150, null=True, verbose_name='URL', blank=True)),
|
||||||
|
('cube', models.CharField(max_length=256, null=True, verbose_name='Cube', blank=True)),
|
||||||
|
('aggregate1', models.CharField(max_length=64, null=True, verbose_name='Aggregate', blank=True)),
|
||||||
|
('drilldown1', models.CharField(max_length=64, null=True, verbose_name='Drilldown 1', blank=True)),
|
||||||
|
('drilldown2', models.CharField(max_length=64, null=True, verbose_name='Drilldown 2', blank=True)),
|
||||||
|
('other_parameters', models.TextField(null=True, verbose_name='Other parameters', blank=True)),
|
||||||
|
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
|
||||||
|
('page', models.ForeignKey(to='data.Page')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Cubes Table',
|
||||||
|
},
|
||||||
|
bases=(models.Model,),
|
||||||
|
),
|
||||||
|
]
|
|
@ -14,9 +14,13 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import json
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from combo.data.models import CellBase
|
from combo.data.models import CellBase
|
||||||
from combo.data.library import register_cell_class
|
from combo.data.library import register_cell_class
|
||||||
|
@ -26,10 +30,8 @@ from combo.data.library import register_cell_class
|
||||||
class Gauge(CellBase):
|
class Gauge(CellBase):
|
||||||
title = models.CharField(_('Title'), max_length=150, blank=True, null=True)
|
title = models.CharField(_('Title'), max_length=150, blank=True, null=True)
|
||||||
url = models.URLField(_('URL'), max_length=150, blank=True, null=True)
|
url = models.URLField(_('URL'), max_length=150, blank=True, null=True)
|
||||||
data_source = models.CharField(_('Data Source'), max_length=150,
|
data_source = models.CharField(_('Data Source'), max_length=150, blank=True, null=True)
|
||||||
blank=True, null=True)
|
jsonp_data_source = models.BooleanField(_('Use JSONP to get data'), default=True)
|
||||||
jsonp_data_source = models.BooleanField(_('Use JSONP to get data'),
|
|
||||||
default=True)
|
|
||||||
max_value = models.PositiveIntegerField(_('Max Value'), blank=True, null=True)
|
max_value = models.PositiveIntegerField(_('Max Value'), blank=True, null=True)
|
||||||
|
|
||||||
template_name = 'combo/gauge-cell.html'
|
template_name = 'combo/gauge-cell.html'
|
||||||
|
@ -55,3 +57,122 @@ class Gauge(CellBase):
|
||||||
'data_source_url': data_source_url,
|
'data_source_url': data_source_url,
|
||||||
'jsonp': self.jsonp_data_source,
|
'jsonp': self.jsonp_data_source,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BaseCubesChart(CellBase):
|
||||||
|
title = models.CharField(_('Title'), max_length=150, blank=True, null=True)
|
||||||
|
url = models.URLField(_('URL'), max_length=150, blank=True, null=True)
|
||||||
|
cube = models.CharField(verbose_name=_('Cube'), max_length=256, blank=True, null=True)
|
||||||
|
aggregate1 = models.CharField(verbose_name=_('Aggregate'), max_length=64, blank=True, null=True)
|
||||||
|
drilldown1 = models.CharField(verbose_name=_('Drilldown 1'), max_length=64, blank=True,
|
||||||
|
null=True)
|
||||||
|
drilldown2 = models.CharField(verbose_name=_('Drilldown 2'), max_length=64, blank=True,
|
||||||
|
null=True)
|
||||||
|
other_parameters = models.TextField(verbose_name=_('Other parameters'), blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_enabled(self):
|
||||||
|
return bool(getattr(settings, 'CUBES_URL', None))
|
||||||
|
|
||||||
|
def get_additional_label(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def get_default_form_class(self):
|
||||||
|
from .forms import CubesBarChartForm
|
||||||
|
return CubesBarChartForm
|
||||||
|
|
||||||
|
def get_cell_extra_context(self):
|
||||||
|
return {
|
||||||
|
'cell': self,
|
||||||
|
'title': self.title,
|
||||||
|
'url': self.url,
|
||||||
|
'aggregate': self.get_aggregate(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_aggregate(self):
|
||||||
|
'''Get aggregate defined by chosen cube and the two drildown paths, request ordering of the
|
||||||
|
data by natural order of each axis.'''
|
||||||
|
from .utils import get_aggregate, get_cube, compute_levels
|
||||||
|
aggregate = get_aggregate(name=self.cube,
|
||||||
|
aggregate1=self.aggregate1,
|
||||||
|
drilldown1=self.drilldown1,
|
||||||
|
drilldown2=self.drilldown2,
|
||||||
|
other_parameters=(json.loads(self.other_parameters) if
|
||||||
|
self.other_parameters else None))
|
||||||
|
cube = get_cube(self.cube)
|
||||||
|
if not aggregate or not cube:
|
||||||
|
return
|
||||||
|
|
||||||
|
label_refs1 = []
|
||||||
|
key_refs1 = []
|
||||||
|
if self.drilldown1:
|
||||||
|
compute_levels(cube, self.drilldown1, label_refs=label_refs1, key_refs=key_refs1)
|
||||||
|
key_refs2 = []
|
||||||
|
label_refs2 = []
|
||||||
|
if self.drilldown2:
|
||||||
|
compute_levels(cube, self.drilldown2, label_refs=label_refs2, key_refs=key_refs2)
|
||||||
|
for ag in cube['aggregates']:
|
||||||
|
if ag['name'] != self.aggregate1:
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
|
||||||
|
def cell_ref(cell, refs):
|
||||||
|
return tuple(cell[ref] for ref in refs)
|
||||||
|
|
||||||
|
keys1 = OrderedDict()
|
||||||
|
labels = OrderedDict()
|
||||||
|
datasets = OrderedDict()
|
||||||
|
|
||||||
|
for cell in aggregate['cells']:
|
||||||
|
label1 = u' / '.join(map(unicode, cell_ref(cell, label_refs1)))
|
||||||
|
key1 = cell_ref(cell, key_refs1)
|
||||||
|
labels[key1] = label1
|
||||||
|
keys1[key1] = 1
|
||||||
|
if key_refs2:
|
||||||
|
label2 = u' / '.join(map(unicode, cell_ref(cell, label_refs2)))
|
||||||
|
key2 = cell_ref(cell, key_refs2)
|
||||||
|
else:
|
||||||
|
label2 = ''
|
||||||
|
key2 = 1
|
||||||
|
dataset = datasets.setdefault(key2, {'label': label2,
|
||||||
|
'data': OrderedDict()})
|
||||||
|
value = cell[self.aggregate1]
|
||||||
|
dataset['data'][key1] = value
|
||||||
|
for dataset in datasets.itervalues():
|
||||||
|
dataset['data'] = [dataset['data'].get(key, 0) for key in keys1]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'labels': labels.values(),
|
||||||
|
'datasets': [{
|
||||||
|
'label': dataset['label'],
|
||||||
|
'data': dataset['data'],
|
||||||
|
} for dataset in datasets.itervalues()]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_cell_class
|
||||||
|
class CubesBarChart(BaseCubesChart):
|
||||||
|
template_name = 'combo/cubes-barchart.html'
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
js = ('xstatic/Chart.min.js', 'js/combo.cubes-barchart.js')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Cubes Barchart')
|
||||||
|
|
||||||
|
def get_cell_extra_context(self):
|
||||||
|
ctx = super(CubesBarChart, self).get_cell_extra_context()
|
||||||
|
# Need JSON serialization to pass data to Chart.js
|
||||||
|
ctx['json_aggregate'] = json.dumps(ctx['aggregate'])
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
@register_cell_class
|
||||||
|
class CubesTable(BaseCubesChart):
|
||||||
|
template_name = 'combo/cubes-table.html'
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Cubes Table')
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
$(function() {
|
||||||
|
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() + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.combo-cube-aggregate').each(function(idx, elem) {
|
||||||
|
var cube_url = $(elem).data('cube-url');
|
||||||
|
var aggregate_url = $(elem).data('aggregate-url');
|
||||||
|
var model = null;
|
||||||
|
var ctx = $('canvas', elem)[0].getContext("2d");
|
||||||
|
var id = $(elem).data('combo-cube-aggregate-id');
|
||||||
|
|
||||||
|
var option = {
|
||||||
|
//Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
|
||||||
|
scaleBeginAtZero : true,
|
||||||
|
|
||||||
|
//Boolean - Whether grid lines are shown across the chart
|
||||||
|
scaleShowGridLines : true,
|
||||||
|
|
||||||
|
//String - Colour of the grid lines
|
||||||
|
scaleGridLineColor : "rgba(0,0,0,.05)",
|
||||||
|
|
||||||
|
//Number - Width of the grid lines
|
||||||
|
scaleGridLineWidth : 1,
|
||||||
|
|
||||||
|
//Boolean - Whether to show horizontal lines (except X axis)
|
||||||
|
scaleShowHorizontalLines: true,
|
||||||
|
|
||||||
|
//Boolean - Whether to show vertical lines (except Y axis)
|
||||||
|
scaleShowVerticalLines: true,
|
||||||
|
|
||||||
|
//Boolean - If there is a stroke on each bar
|
||||||
|
barShowStroke : true,
|
||||||
|
|
||||||
|
//Number - Pixel width of the bar stroke
|
||||||
|
barStrokeWidth : 2,
|
||||||
|
|
||||||
|
//Number - Spacing between each of the X value sets
|
||||||
|
barValueSpacing : 5,
|
||||||
|
|
||||||
|
//Number - Spacing between data sets within X values
|
||||||
|
barDatasetSpacing : 1,
|
||||||
|
|
||||||
|
//String - A legend template
|
||||||
|
legendTemplate : "ul",
|
||||||
|
multiTooltipTemplate: "<%= datasetLabel %>: <%= value %>",
|
||||||
|
responsive: true,
|
||||||
|
}
|
||||||
|
var data = window['combo_cube_aggregate_' + id];
|
||||||
|
// Set one color by dataset
|
||||||
|
var n = data.datasets.length;
|
||||||
|
for (var i = 0; i < n; i++) {
|
||||||
|
var dataset = data.datasets[i];
|
||||||
|
$.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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
var clone = function(obj){
|
||||||
|
var objClone = {};
|
||||||
|
for (var key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) objClone[key] = obj[key];
|
||||||
|
};
|
||||||
|
return objClone;
|
||||||
|
}
|
||||||
|
var chart = new Chart(ctx).Bar(data, option);
|
||||||
|
if (chart.datasets.length == 1) {
|
||||||
|
// Set one color by bar
|
||||||
|
var n = chart.datasets[0].bars.length;
|
||||||
|
for (var i = 0; i < n; i++) {
|
||||||
|
var bar = chart.datasets[0].bars[i];
|
||||||
|
$.extend(bar, {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
bar['_saved'] = clone(bar);
|
||||||
|
bar.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.chart = chart;
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,12 @@
|
||||||
|
<script>
|
||||||
|
var combo_cube_aggregate_{{ cell.id }} = {{ json_aggregate|safe }};
|
||||||
|
</script>
|
||||||
|
<div
|
||||||
|
class="combo-cube-aggregate"
|
||||||
|
data-combo-cube-aggregate-id="{{ cell.id }}">
|
||||||
|
{% if title %}
|
||||||
|
{% if url %}<a href="{{url}}">{% endif %}{{title}}{% if url %}</a>{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<canvas style="width: 100%;">
|
||||||
|
</canvas>
|
||||||
|
</div>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<table class="combo-cube-table">
|
||||||
|
{% if title %}
|
||||||
|
<caption>
|
||||||
|
{% if url %}<a href="{{url}}">{% endif %}{{title}}{% if url %}</a>{% endif %}
|
||||||
|
</caption>
|
||||||
|
{% endif %}
|
||||||
|
<thead>
|
||||||
|
<th></th>
|
||||||
|
{% for label in aggregate.labels %}
|
||||||
|
<td>{{ label }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for dataset in aggregate.datasets %}
|
||||||
|
<tr>
|
||||||
|
<th>{{ dataset.label }}</th>
|
||||||
|
{% for value in dataset.data %}
|
||||||
|
<td>{{ value }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
|
@ -18,7 +18,8 @@ from django.conf.urls import patterns, url
|
||||||
|
|
||||||
from .views import ajax_gauge_count
|
from .views import ajax_gauge_count
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns(
|
||||||
|
'',
|
||||||
url(r'^ajax/gauge-count/(?P<cell>[\w_-]+)/$',
|
url(r'^ajax/gauge-count/(?P<cell>[\w_-]+)/$',
|
||||||
ajax_gauge_count, name='combo-ajax-gauge-count'),
|
ajax_gauge_count, name='combo-ajax-gauge-count'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
# combo - content management system
|
||||||
|
# Copyright (C) 2015 Entr'ouvert
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it
|
||||||
|
# under the terms of the GNU Affero General Public License as published
|
||||||
|
# by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def get_requests_params():
|
||||||
|
return getattr(settings, 'CUBES_REQUEST_PARAMS', {})
|
||||||
|
|
||||||
|
|
||||||
|
def get_cubes():
|
||||||
|
try:
|
||||||
|
r = requests.get(urlparse.urljoin(settings.CUBES_URL, 'cubes'), **get_requests_params)
|
||||||
|
except RequestException:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
return r.json()
|
||||||
|
except ValueError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_cube(name):
|
||||||
|
model_url = urlparse.urljoin(settings.CUBES_URL, 'cube/%s/model' % name)
|
||||||
|
try:
|
||||||
|
r = requests.get(model_url, **get_requests_params)
|
||||||
|
except RequestException:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return r.json()
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_drilldown(name):
|
||||||
|
cube = get_cube(name)
|
||||||
|
if not cube:
|
||||||
|
return []
|
||||||
|
l = []
|
||||||
|
for dimension in cube.get('dimensions', []):
|
||||||
|
dim_name = dimension['name']
|
||||||
|
dim_label = dimension.get('label') or dim_name
|
||||||
|
if dimension.get('levels'):
|
||||||
|
levels = {}
|
||||||
|
for level in dimension['levels']:
|
||||||
|
levels[level['name']] = level.get('label') or level['name']
|
||||||
|
if dimension.get('hierarchies'):
|
||||||
|
for hierarchy in dimension['hierarchies']:
|
||||||
|
h_name = hierarchy['name']
|
||||||
|
h_label = hierarchy.get('label') or h_name
|
||||||
|
if h_name == 'default':
|
||||||
|
h_label = ''
|
||||||
|
for level in hierarchy['levels']:
|
||||||
|
labels = filter(None, [dim_label, h_label, levels[level]])
|
||||||
|
label = ' - '.join(labels)
|
||||||
|
name = '%s@%s:%s' % (dim_name, h_name, level)
|
||||||
|
l.append((name, label))
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
else:
|
||||||
|
l.append((dim_name, dim_label))
|
||||||
|
return l
|
||||||
|
|
||||||
|
|
||||||
|
def compute_levels(cube, drilldown, key_refs=None, label_refs=None):
|
||||||
|
from .utils import get_attribute_ref
|
||||||
|
dim = drilldown.split('@')[0]
|
||||||
|
hier = drilldown.split('@')[1].split(':')[0]
|
||||||
|
lev = drilldown.split(':')[1]
|
||||||
|
|
||||||
|
for dimension in cube['dimensions']:
|
||||||
|
if dimension['name'] != dim:
|
||||||
|
continue
|
||||||
|
level_label_refs = {}
|
||||||
|
level_key_refs = {}
|
||||||
|
for level in dimension['levels']:
|
||||||
|
level_key_refs[level['name']] = get_attribute_ref(level, level['key'])
|
||||||
|
level_label_refs[level['name']] = get_attribute_ref(level, level['label_attribute'])
|
||||||
|
for hierarchy in dimension['hierarchies']:
|
||||||
|
if hierarchy['name'] != hier:
|
||||||
|
continue
|
||||||
|
for level in hierarchy['levels']:
|
||||||
|
if key_refs is not None:
|
||||||
|
key_refs.append(level_key_refs[level])
|
||||||
|
if label_refs is not None:
|
||||||
|
label_refs.append(level_label_refs[level])
|
||||||
|
if level == lev:
|
||||||
|
break
|
||||||
|
break
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def get_aggregate(name, aggregate1, drilldown1, drilldown2, other_parameters=None):
|
||||||
|
if not name:
|
||||||
|
return None
|
||||||
|
cube = get_cube(name)
|
||||||
|
aggregate_url = urlparse.urljoin(settings.CUBES_URL, 'cube/%s/aggregate' % name)
|
||||||
|
if not aggregate1:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
params = {'aggregate': aggregate1}
|
||||||
|
drilldowns = []
|
||||||
|
key_refs = []
|
||||||
|
if drilldown1:
|
||||||
|
compute_levels(cube, drilldown1, key_refs=key_refs)
|
||||||
|
drilldowns.append(drilldown1)
|
||||||
|
if drilldown2:
|
||||||
|
compute_levels(cube, drilldown2, key_refs=key_refs)
|
||||||
|
drilldowns.append(drilldown2)
|
||||||
|
if drilldowns:
|
||||||
|
params['drilldown'] = drilldowns
|
||||||
|
|
||||||
|
if key_refs:
|
||||||
|
params['order'] = key_refs
|
||||||
|
if other_parameters:
|
||||||
|
params.update(other_parameters)
|
||||||
|
|
||||||
|
r = requests.get(aggregate_url, params=params, **get_requests_params)
|
||||||
|
except RequestException:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return r.json()
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_attribute_ref(level, name):
|
||||||
|
for attribute in level['attributes']:
|
||||||
|
if attribute['name'] == name:
|
||||||
|
return attribute['ref']
|
|
@ -14,11 +14,9 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import json
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from django.conf import settings
|
from django.http import HttpResponse
|
||||||
from django.http import Http404, HttpResponse
|
|
||||||
|
|
||||||
from .models import Gauge
|
from .models import Gauge
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,8 @@ INSTALLED_APPS = (
|
||||||
'combo.apps.wcs',
|
'combo.apps.wcs',
|
||||||
'combo.apps.publik',
|
'combo.apps.publik',
|
||||||
'combo.apps.family',
|
'combo.apps.family',
|
||||||
|
'combo.apps.dataviz',
|
||||||
|
'xstatic.pkg.chart_js',
|
||||||
)
|
)
|
||||||
|
|
||||||
INSTALLED_APPS = plugins.register_plugins_apps(INSTALLED_APPS)
|
INSTALLED_APPS = plugins.register_plugins_apps(INSTALLED_APPS)
|
||||||
|
|
Loading…
Reference in New Issue