dataviz: adapt chart's height to content (#62282) #54

Merged
smihai merged 4 commits from wip/62282-dataviz-allow-charts-auto-height into main 2023-03-27 11:43:58 +02:00
3 changed files with 180 additions and 22 deletions

View File

@ -360,7 +360,6 @@ class ChartNgCell(CellBase):
if self.statistic.service_slug == 'bijoe':
x_labels, y_labels, data = self.parse_response(response, chart)
chart.x_labels = x_labels
self.prepare_chart(chart, width, height)
if chart.axis_count == 1:
data = self.process_one_dimensional_data(chart, data)

Depuis les nouvelles versions du patch cette condition semble superflue.

Depuis les nouvelles versions du patch cette condition semble superflue.
@ -379,7 +378,6 @@ class ChartNgCell(CellBase):
chart.x_labels = data['x_labels']
chart.axis_count = min(len(data['series']), 2)
self.prepare_chart(chart, width, height)
if self.statistic.data_type:
chart.config.value_formatter = self.get_value_formatter(self.statistic.data_type)
@ -401,7 +399,7 @@ class ChartNgCell(CellBase):
for serie in data['series']:
chart.add(serie['label'], serie['data'])
self.configure_chart(chart, width, height)
return chart
def get_filter_params(self):
@ -531,14 +529,37 @@ class ChartNgCell(CellBase):
return x_labels, y_labels, data
def prepare_chart(self, chart, width, height):
def configure_dot_chart(self, chart, width, height):
chart.show_legend = False
# use a single colour for dots
chart.config.style.colors = ('#1f77b4',) * max(len(chart.x_labels), 1)
def configure_horizontal_bar_chart(self, chart, width, height):
Review

Il faut garder la condition width < 500, le fait que ça passe sans traduit juste un manque de test.

Il faut garder la condition `width < 500`, le fait que ça passe sans traduit juste un manque de test.
Review

Bien vu.
J'ai remis la condition width < 500 et rajouté un test.

Bien vu. J'ai remis la condition `width < 500` et rajouté un test.
if width and width < 500:
# truncate labels
chart.x_labels = [pygal.util.truncate(x, 15) for x in chart.x_labels]
def configure_pie_chart(self, chart, width, height):
chart.show_legend = True
if width and height:
# pies are as tall as wide, reserve the appropriate space and distribute
# the rest for the legend.
chart.truncate_legend = (width - height) // 10
elif width:
chart.truncate_legend = width // 20
def configure_chart(self, chart, width, height):
auto_height_scale = pygal.style.DefaultStyle.legend_font_size * 1.75
chart.config.margin = 0
if width:
chart.config.width = width
if height:
chart.config.height = height
height = height or int(self.height)
Review

J'aime bien ce changement, par soucis de cohérence ça serait cool d'avoir la même chose pour width genre juste en remplaçant le bloc if width: du dessus par width = width or int(self.width) (en plus de la modif à DatavizGraphView). Idéalement ce changement irait dans un commit séparé, mais pas très important.

J'aime bien ce changement, par soucis de cohérence ça serait cool d'avoir la même chose pour width genre juste en remplaçant le bloc `if width:` du dessus par `width = width or int(self.width)` (en plus de la modif à DatavizGraphView). Idéalement ce changement irait dans un commit séparé, mais pas très important.
Review

Fait un commit séparé pour height. En revanche self.width n'existe pas.

Fait un commit séparé pour `height`. En revanche `self.width` n'existe pas.
Review

Ah oui, désolé pour l'embrouille

Ah oui, désolé pour l'embrouille
# adapt chart's height to legend length
chart.config.height = max(height, auto_height_scale * len(chart.raw_series))
if width or height:
chart.config.explicit_size = True
chart.config.js = [os.path.join(settings.STATIC_URL, 'js/pygal-tooltips.js')]

À quoi sert cette ligne ?

À quoi sert cette ligne ?

A garder la hauteur définie de la cellule lorsque

chart.legend_at_bottom = True
A garder la hauteur définie de la cellule lorsque ``` chart.legend_at_bottom = True ```

Ah oui désolé j'aurais pu m'en apercevoir... Ça serait bien d'avoir un test si tu te sens.

Et c'est aussi un endroit où on perd la valeur du paramètre height passé dans la requête, il ne faudrait pas.

Ah oui désolé j'aurais pu m'en apercevoir... Ça serait bien d'avoir un test si tu te sens. Et c'est aussi un endroit où on perd la valeur du paramètre `height` passé dans la requête, il ne faudrait pas.

Cool je vois le nouveau test, par contre la remarque sur conserver le paramètre height me paraît toujours importante (tu peux voir dans le /manage/ que ça semi-casse le rendu du graphe).

Cool je vois le nouveau test, par contre la remarque sur conserver le paramètre `height` me paraît toujours importante (tu peux voir dans le `/manage/` que ça semi-casse le rendu du graphe).
chart.show_legend = bool(chart.axis_count > 1)
Review

Mais le graphe de type pie peut également voir sa légende déborder en hauteur, non ? (peut-être que je me plante)

Si c'est le cas il doit bénéficier de la correction du patch, et donc ce serait plutôt se placer sous ce bloc et faire

if not chart.legend_at_bottom:
    # adapt height
    height = max(...
Mais le graphe de type pie peut également voir sa légende déborder en hauteur, non ? (peut-être que je me plante) Si c'est le cas il doit bénéficier de la correction du patch, et donc ce serait plutôt se placer sous ce bloc et faire ``` if not chart.legend_at_bottom: # adapt height height = max(... ```
Review

Mais le graphe de type pie peut également voir sa légende déborder en hauteur, non ? (peut-être que je me plante)

En effet, il restait ce cas.
Je l'ai pris en compte et rajouté un test.

Pas repris ta suggestion d'implémentation car chart.legend_at_bottom lève un AttributeError.

> Mais le graphe de type pie peut également voir sa légende déborder en hauteur, non ? (peut-être que je me plante) > En effet, il restait ce cas. Je l'ai pris en compte et rajouté un test. Pas repris ta suggestion d'implémentation car `chart.legend_at_bottom` lève un `AttributeError`.
@ -557,25 +578,15 @@ class ChartNgCell(CellBase):
'#17becf',
)
if self.chart_type == 'dot':
chart.show_legend = False
# use a single colour for dots
chart.config.style.colors = ('#1f77b4',) * max(len(chart.x_labels), 1)
custom_configure_method_name = 'configure_%s_chart' % self.chart_type.replace('-', '_')
if hasattr(self, custom_configure_method_name):
getattr(self, custom_configure_method_name)(chart, width, height)
if self.chart_type != 'pie':
if width and width < 500:
Review

C'est sur cette ligne que portait mon unique réserve sur le patch précédent, je ne sais pas pourquoi elle est encore là ?

En plus avant c'était chart.config.height = int(self.height), maintenant le chart.config a disparu donc la ligne ne fait plus rien.

Encore une fois le bug c'est qu'en rendu BO ça va produire un graphe trop haut. Il faut que cette ligne restaure la valeur originale de height avant correction (self.height ou la valeur transmise en paramètre, selon les cas). Ça serait bien aussi d'ajouter un test là dessus.

C'est sur cette ligne que portait mon unique réserve sur le patch précédent, je ne sais pas pourquoi elle est encore là ? En plus avant c'était `chart.config.height = int(self.height)`, maintenant le `chart.config` a disparu donc la ligne ne fait plus rien. Encore une fois le bug c'est qu'en rendu BO ça va produire un graphe trop haut. Il faut que cette ligne restaure la valeur originale de height avant correction (self.height ou la valeur transmise en paramètre, selon les cas). Ça serait bien aussi d'ajouter un test là dessus.
Review

Je me plantais à chaque fois ici, fatigue.

Je me plantais à chaque fois ici, fatigue.
chart.legend_at_bottom = True
if self.chart_type == 'horizontal-bar':
# truncate labels
chart.x_labels = [pygal.util.truncate(x, 15) for x in chart.x_labels]
else:
chart.show_legend = True
if width and height:
# pies are as tall as wide, reserve the appropriate space and distribute
# the rest for the legend.
chart.truncate_legend = (width - height) // 10
elif width:
chart.truncate_legend = width // 20
# restore demanded chart's height
chart.config.height = height
def process_one_dimensional_data(self, chart, data):
if self.hide_null_values:

View File

@ -73,7 +73,7 @@ class DatavizGraphView(DetailView):
try:
chart = form.instance.get_chart(
width=int(request.GET['width']) if request.GET.get('width') else None,
height=int(request.GET['height']) if request.GET.get('height') else int(self.cell.height),
height=int(request.GET['height']) if request.GET.get('height') else None,
)
except UnsupportedDataSet:
return self.error(_('Unsupported dataset.'))

View File

@ -4,6 +4,7 @@ import json
import urllib.parse
from unittest import mock
import lxml
import pytest
from django.apps import apps
from django.contrib.auth.models import Group
@ -149,6 +150,12 @@ VISUALIZATION_JSON = [
'name': 'sixteenth visualization (numerical labels)',
'slug': 'sixteenth',
},
{
'data-url': 'https://bijoe.example.com/visualization/17/json/',
'path': 'https://bijoe.example.com/visualization/17/iframe/?signature=123',
'name': 'seventeenth visualization (string labels)',
'slug': 'seventeenth',
},
]
@ -324,6 +331,83 @@ def bijoe_mock(url, request):
'measure': 'integer',
}
return {'content': json.dumps(response), 'request': request, 'status_code': 200}
if url.path == '/visualization/17/json/':
response = {
'data': [
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
],
'axis': {
'x_labels': [
'Mois de %s' % m
for m in [
'janvier',
'février',
'mars',
'avril',
'mai',
'juin',
'juillet',
'août',
'septembre',
'octobre',
'novembre',
'décembre',
]
],
'y_labels': ['Label %s' % i for i in range(0, 9)],
},
'format': '1',
'unit': None,
'measure': 'integer',
}
return {'content': json.dumps(response), 'request': request, 'status_code': 200}
if url.path == '/visualization/17/json/':
response = {
'data': [
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
],
'axis': {
'x_labels': [
'Mois de %s' % m
for m in [
'janvier',
'février',
'mars',
'avril',
'mai',
'juin',
'juillet',
'août',
'septembre',
'octobre',
'novembre',
'décembre',
]
],
'y_labels': ['Label %s' % i for i in range(0, 9)],
},
'format': '1',
'unit': None,
'measure': 'integer',
}
return {'content': json.dumps(response), 'request': request, 'status_code': 200}
STATISTICS_LIST = {
@ -1345,6 +1429,69 @@ def test_chartng_cell_view(app, normal_user, statistics):
assert 'not found' in resp.text
@with_httmock(bijoe_mock)
def test_chartng_cell_with_truncated_labels(app, normal_user, statistics):
page = Page(title='One', slug='index')
page.save()
cell = ChartNgCell(page=page, order=1, placeholder='content', chart_type='horizontal-bar')
cell.statistic = Statistic.objects.get(slug='seventeenth')
cell.save()
location = '/api/dataviz/graph/%s/' % cell.id
resp = app.get(location + '?width=499')
svg = lxml.etree.fromstring(resp.text.encode())
for label in svg.xpath(
"//ns:g[@class='legends']//ns:text", namespaces={'ns': 'http://www.w3.org/2000/svg'}
):
assert len(label) <= 15
@with_httmock(bijoe_mock)
def test_chartng_cell_viewbox(app, normal_user, statistics):
page = Page(title='One', slug='index')
page.save()
cell = ChartNgCell(page=page, order=1, placeholder='content')
cell.statistic = Statistic.objects.get(slug='eighth')
cell.save()
location = '/api/dataviz/graph/%s/' % cell.id
resp = app.get(location + '?width=600')
svg = lxml.etree.fromstring(resp.text.encode())
assert svg.attrib['width'] == '600'
assert svg.attrib['height'] == '250'
cell.statistic = Statistic.objects.get(slug='seventeenth')
cell.save()
resp = app.get(location + '?width=800&height=600')
svg = lxml.etree.fromstring(resp.text.encode())
assert svg.attrib['width'] == '800'
assert svg.attrib['height'] == '600'
# simulate chart preview from backoffice
resp = app.get(location + '?width=300&height=150')
svg = lxml.etree.fromstring(resp.text.encode())
assert svg.attrib['width'] == '300'
assert svg.attrib['height'] == '150'
# reduce cell's height
cell.height = '150'
cell.save()
resp = app.get(location + '?width=600&height=200')
svg = lxml.etree.fromstring(resp.text.encode())
# rendered cell should be higher than defined height
assert svg.attrib['height'] == '220.5'
# test pie chart rendering
cell.chart_type = 'pie'
cell.save()
resp = app.get(location + '?width=600&height=200')
svg = lxml.etree.fromstring(resp.text.encode())
# rendered cell should be higher than defined height
assert svg.attrib['height'] == '220.5'
@with_httmock(new_api_mock)
def test_chartng_cell_view_new_api(app, normal_user, new_api_statistics):
page = Page.objects.create(title='One', slug='index')