dataviz: allow control of total display in tables (#85654)
gitea/combo/pipeline/head This commit looks good Details

This commit is contained in:
Valentin Deniaud 2024-02-13 15:50:33 +01:00
parent 5e8b58c6ca
commit 6be1d6c5fc
7 changed files with 187 additions and 9 deletions

View File

@ -186,6 +186,7 @@ class ChartNgForm(ChartFiltersMixin, forms.ModelForm):
'time_range_start_template',
'time_range_end_template',
'chart_type',
'display_total',
'height',
'sort_order',
'hide_null_values',
@ -217,6 +218,7 @@ class ChartNgForm(ChartFiltersMixin, forms.ModelForm):
'time_range_end',
'time_range_start_template',
'time_range_end_template',
'display_total',
):
del self.fields[field]
else:
@ -228,6 +230,9 @@ class ChartNgForm(ChartFiltersMixin, forms.ModelForm):
del self.fields['time_range_start_template']
del self.fields['time_range_end_template']
if not self.instance.is_table_chart() or self.instance.statistic.data_type:
del self.fields['display_total']
def add_filter_fields(self):
new_fields = OrderedDict()
for field_name, field in self.fields.items():

View File

@ -0,0 +1,27 @@
# Generated by Django 3.2.18 on 2024-02-14 11:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dataviz', '0028_increase_extra_css_class'),
]
operations = [
migrations.AddField(
model_name='chartngcell',
name='display_total',
field=models.CharField(
choices=[
('none', 'None'),
('line-and-column', 'Total line and total column'),
('line', 'Total line'),
('column', 'Total column'),
],
default='line-and-column',
max_length=20,
verbose_name='Display of total',
),
),
]

View File

@ -246,6 +246,17 @@ class ChartNgCell(CellBase):
('table-inverted', _('Table (inverted)')),
),
)
display_total = models.CharField(
_('Display of total'),
max_length=20,
default='line-and-column',
choices=(
('none', _('None')),
('line-and-column', _('Total line and total column')),
('line', _('Total line')),
('column', _('Total column')),
),
)
height = models.CharField(
_('Height'),
@ -381,6 +392,8 @@ class ChartNgCell(CellBase):
if chart.axis_count == 1:
data = self.process_one_dimensional_data(chart, data)
if getattr(chart, 'compute_sum', True) and self.is_table_chart():
data = self.add_total_to_line_table(chart, data)
self.add_data_to_chart(chart, data, y_labels)
else:
data = response['data']
@ -396,10 +409,10 @@ class ChartNgCell(CellBase):
chart.x_labels = data['x_labels']
chart.axis_count = min(len(data['series']), 2)
chart.compute_sum = False
if self.statistic.data_type:
chart.config.value_formatter = self.get_value_formatter(self.statistic.data_type)
chart.compute_sum = False
if chart.axis_count == 1:
data['series'][0]['data'] = self.process_one_dimensional_data(
@ -419,6 +432,10 @@ class ChartNgCell(CellBase):
for serie in data['series']:
chart.add(serie['label'], serie['data'])
if self.is_table_chart() and not self.statistic.data_type:
self.add_total_to_table(chart, [serie['data'] for serie in data['series']])
self.configure_chart(chart, width, height)
return chart
@ -614,8 +631,6 @@ class ChartNgCell(CellBase):
data = self.hide_values(chart, data)
if data and self.sort_order != 'none':
data = self.sort_values(chart, data)
if getattr(chart, 'compute_sum', True) and self.is_table_chart():
data = self.add_total_to_line_table(chart, data)
return data
@staticmethod
@ -660,6 +675,28 @@ class ChartNgCell(CellBase):
chart.x_labels.append(gettext('Total'))
return data
def add_total_to_table(self, chart, series_data):
if chart.axis_count == 0:
return
# do not add total for single point
if len(series_data) == 1 and len(series_data[0]) == 1:
return
if self.display_total in ('line', 'line-and-column'):
chart.x_labels.append(gettext('Total'))
for serie in series_data:
serie.append(sum(x for x in serie if x is not None))
if chart.axis_count == 1:
return
if self.display_total in ('column', 'line-and-column'):
line_totals = []
for line in zip(*series_data):
line_totals.append(sum(x for x in line if x is not None))
chart.add(gettext('Total'), line_totals)
def add_data_to_chart(self, chart, data, y_labels):
if self.chart_type != 'pie':
series_data = []

View File

@ -13,3 +13,9 @@
font-family: FontAwesome;
content: "\f019"; /* download */
}
.dataviz-table.total-line tr:last-child,
.dataviz-table.total-line-and-column tr:last-child {
font-weight: 600;
background: #f7f7f7;
}

View File

@ -1,7 +1,7 @@
{% load i18n %}
{% if cell.title %}<h2>{{cell.title}}</h2>{% endif %}
{% if cell.is_table_chart %}
<div id="chart-{{cell.id}}" class="dataviz-table"></div>
<div id="chart-{{cell.id}}" class="dataviz-table total-{{ cell.display_total }}"></div>
<script>
$(function() {
var extra_context = $('#chart-{{cell.id}}').parents('.cell').data('extra-context');

View File

@ -111,7 +111,7 @@ class DatavizGraphView(DetailView):
rendered = chart.render_table(
transpose=bool(self.cell.chart_type == 'table-inverted'),
total=getattr(chart, 'compute_sum', True),
total=bool(self.cell.statistic.service_slug == 'bijoe' and chart.compute_sum),
)
rendered = rendered.replace('<table>', '<table class="main">')
return HttpResponse(rendered)

View File

@ -1669,10 +1669,11 @@ def test_chartng_cell_view_new_api_export(app, normal_user, new_api_statistics):
data = pyexcel_ods.get_data(io.BytesIO(ods_resp.body))
assert data['Two series stat'] == [
['', 'Serie 1', 'Serie 2'],
['2020-10', 0, 2],
['2020-11', 16, 1],
['2020-12', 2, 0],
['', 'Serie 1', 'Serie 2', 'Total'],
['2020-10', 0, 2, 2],
['2020-11', 16, 1, 17],
['2020-12', 2, 0, 2],
['Total', 18, 3, 21],
]
cell.statistic = Statistic.objects.get(slug='empty-x-labels')
@ -1683,6 +1684,86 @@ def test_chartng_cell_view_new_api_export(app, normal_user, new_api_statistics):
assert data['Empty x_labels'] == [['Serie 1'], [4242]]
@with_httmock(new_api_mock)
@pytest.mark.freeze_time('2023-09-28')
def test_chartng_cell_view_new_api_export_ods_total(app, normal_user, new_api_statistics):
page = Page.objects.create(title='One', slug='index')
cell = ChartNgCell(page=page, order=1, chart_type='table', placeholder='content')
cell.statistic = Statistic.objects.get(slug='two-series')
cell.save()
resp = app.get('/')
resp = resp.click('Download')
resp.form['export_format'] = 'ods'
ods_resp = resp.form.submit().follow()
data = pyexcel_ods.get_data(io.BytesIO(ods_resp.body))
assert data['Two series stat'] == [
['', 'Serie 1', 'Serie 2', 'Total'],
['2020-10', 0, 2, 2],
['2020-11', 16, 1, 17],
['2020-12', 2, 0, 2],
['Total', 18, 3, 21],
]
cell.display_total = 'line'
cell.save()
ods_resp = resp.form.submit().follow()
data = pyexcel_ods.get_data(io.BytesIO(ods_resp.body))
assert data['Two series stat'] == [
['', 'Serie 1', 'Serie 2'],
['2020-10', 0, 2],
['2020-11', 16, 1],
['2020-12', 2, 0],
['Total', 18, 3],
]
cell.display_total = 'column'
cell.save()
ods_resp = resp.form.submit().follow()
data = pyexcel_ods.get_data(io.BytesIO(ods_resp.body))
assert data['Two series stat'] == [
['', 'Serie 1', 'Serie 2', 'Total'],
['2020-10', 0, 2, 2],
['2020-11', 16, 1, 17],
['2020-12', 2, 0, 2],
]
cell.display_total = 'none'
cell.save()
ods_resp = resp.form.submit().follow()
data = pyexcel_ods.get_data(io.BytesIO(ods_resp.body))
assert data['Two series stat'] == [
['', 'Serie 1', 'Serie 2'],
['2020-10', 0, 2],
['2020-11', 16, 1],
['2020-12', 2, 0],
]
# total is not computed for statistics with data type
cell.statistic = Statistic.objects.get(slug='duration-in-seconds')
cell.display_total = 'line'
cell.save()
ods_resp = resp.form.submit().follow()
data = pyexcel_ods.get_data(io.BytesIO(ods_resp.body))
assert data['Duration in seconds'] == [
['', 'Serie 1'],
['Minutes', 140],
['Hours', 10800],
['Days', 345600],
['Months', 8640000],
]
@with_httmock(bijoe_mock)
def test_chartng_cell_manager(app, admin_user, statistics):
page = Page(title='One', slug='index')
@ -1703,6 +1784,7 @@ def test_chartng_cell_manager(app, admin_user, statistics):
assert field_prefix + 'time_range_end' not in resp.form.fields
assert field_prefix + 'time_range_start_template' not in resp.form.fields
assert field_prefix + 'time_range_end_template_end' not in resp.form.fields
assert field_prefix + 'display_total' not in resp.form.fields
cell.statistic = Statistic.objects.get(slug='example')
cell.save()
@ -1719,6 +1801,7 @@ def test_chartng_cell_manager(app, admin_user, statistics):
assert field_prefix + 'time_range_end' not in resp.form.fields
assert field_prefix + 'time_range_start_template' not in resp.form.fields
assert field_prefix + 'time_range_end_template_end' not in resp.form.fields
assert field_prefix + 'display_total' not in resp.form.fields
cell.statistic = Statistic.objects.get(slug='unavailable-stat')
cell.save()
@ -2194,6 +2277,26 @@ def test_chartng_cell_manager_new_api_tabs(app, admin_user):
assert resp.pyquery('[data-tab-slug="appearance"] input[name$="title"]')
@with_httmock(new_api_mock)
@pytest.mark.freeze_time('2021-10-06')
def test_chartng_cell_manager_new_api_table_chart(app, admin_user, new_api_statistics):
page = Page.objects.create(title='One', slug='index')
cell = ChartNgCell(page=page, order=1, placeholder='content')
cell.statistic = Statistic.objects.get(slug='one-serie')
cell.save()
app = login(app)
resp = app.get('/manage/pages/%s/' % page.id)
field_prefix = 'cdataviz_chartngcell-%s-' % cell.id
assert field_prefix + 'display_total' not in resp.form.fields
resp.form[field_prefix + 'chart_type'] = 'table'
manager_submit_cell(resp.form)
assert resp.form[field_prefix + 'display_total'].value == 'line-and-column'
@with_httmock(bijoe_mock)
def test_table_cell(app, admin_user, statistics):
page = Page(title='One', slug='index')