From 8f6091fa2eb4144066f0769216fa25fd0ac50d1f Mon Sep 17 00:00:00 2001 From: Magnus Hagander Date: Tue, 13 Feb 2024 19:42:45 +0100 Subject: [PATCH] Replace Google charts javascript with local SVG charts These charts are (of course) not quite as advanced as the Google ones, but avoids an external depdendency on Google and indeed the local js completely. Also makes for a much faster rendering of the pages with feedback. This leaves the admin-only time graph as the only jsapi depdendency on google, to be handled later. --- media/css/confadmin.css | 26 ++++ media/css/pgeuconf.css | 34 +++++ postgresqleu/confreg/feedback.py | 2 +- postgresqleu/confreg/jinjafunc.py | 6 + postgresqleu/confreg/views.py | 19 ++- postgresqleu/util/templatetags/svgcharts.py | 117 ++++++++++++++++++ template.jinja/confreg/session_feedback.html | 68 ++-------- .../confreg/admin_conference_feedback.html | 45 +------ template/util/svgbarchart.svg | 16 +++ template/util/svgpiechart.svg | 14 +++ 10 files changed, 241 insertions(+), 106 deletions(-) create mode 100644 postgresqleu/util/templatetags/svgcharts.py create mode 100644 template/util/svgbarchart.svg create mode 100644 template/util/svgpiechart.svg diff --git a/media/css/confadmin.css b/media/css/confadmin.css index 960c005f..07bd4cfd 100644 --- a/media/css/confadmin.css +++ b/media/css/confadmin.css @@ -262,6 +262,32 @@ div.pdf_fields_field { cursor: move; } +/* Feedback graphs */ +div.feedbackchart svg .svgpielegend { + font: 13px sans-serif; +} +div.feedbackchart svg .svgpiepercent { + font: 11px sans-serif; +} +div.chartdiv { + margin-bottom: 50px; +} +div.chartdiv h5 { + font-weight: bold; +} +div.chartdiv svg { + margin-top: 10px; + height: 200px; +} +div.chartdiv svg path.pieslice:hover { + stroke-width: 10; + stroke-opacity: 0.5; +} +div.chartdiv svg text { + cursor: default; +} + + /* * Sponsor admin */ diff --git a/media/css/pgeuconf.css b/media/css/pgeuconf.css index 424240ec..99c63d15 100644 --- a/media/css/pgeuconf.css +++ b/media/css/pgeuconf.css @@ -110,3 +110,37 @@ table.versiontable tr td,th { textarea.hiddenfield { display: none; } + + +/* Session feedback */ +div.feedbackchart { + width: 47%; + display: inline-block; + margin-bottom: 20px; + padding-right: 10px; +} +div.feedbackchart h5 { + font-weight: bold; + margin-left: 5em; + margin-bottom: 1em; +} +div.feedbackbarchart h5 { + text-align: center; +} +div.feedbackchart svg path.pieslice:hover { + stroke-width: 10; + stroke-opacity: 0.5; +} +div.feedbackchart svg rect.chartbar:hover { + stroke-width: 1.5; + stroke-opacity: 0.5; +} +div.feedbackchart svg text { + cursor: default; +} +div.feedbackchart svg .svgpielegend { + font: 13px sans-serif; +} +div.feedbackchart svg .svgpiepercent { + font: 11px sans-serif; +} diff --git a/postgresqleu/confreg/feedback.py b/postgresqleu/confreg/feedback.py index fb0c7138..efe17346 100644 --- a/postgresqleu/confreg/feedback.py +++ b/postgresqleu/confreg/feedback.py @@ -14,7 +14,7 @@ def build_graphdata(answers, options): if answers: for a in answers: optionhash[a] += 1 - return iter(optionhash.items()) + return optionhash def feedback_report(request, confname): diff --git a/postgresqleu/confreg/jinjafunc.py b/postgresqleu/confreg/jinjafunc.py index 7dd51753..cd184eaa 100644 --- a/postgresqleu/confreg/jinjafunc.py +++ b/postgresqleu/confreg/jinjafunc.py @@ -19,6 +19,7 @@ from Cryptodome.Hash import SHA from postgresqleu.confreg.templatetags.currency import format_currency from postgresqleu.confreg.templatetags.leadingnbsp import leadingnbsp from postgresqleu.confreg.templatetags.formutil import field_class +from postgresqleu.util.templatetags import svgcharts from postgresqleu.util.templatetags.assets import do_render_asset from postgresqleu.util.messaging import get_messaging_class_from_typename @@ -289,6 +290,10 @@ extra_filters = { 'social_links': filter_social_links, } +extra_globals = { + 'svgcharts': svgcharts, +} + # We can resolve assets only when the template is in our main site. Anything running with # deploystatic is going to have to solve this outside anyway. That means we can safely @@ -308,6 +313,7 @@ def render_jinja_conference_template(conference, templatename, dictionary, disab extensions=['jinja2.ext.with_'], ) env.filters.update(extra_filters) + env.globals.update(extra_globals) t = env.get_template(templatename) diff --git a/postgresqleu/confreg/views.py b/postgresqleu/confreg/views.py index 10cc69bb..67166382 100644 --- a/postgresqleu/confreg/views.py +++ b/postgresqleu/confreg/views.py @@ -80,7 +80,8 @@ from postgresqleu.util.qr import generate_base64_qr from decimal import Decimal from operator import itemgetter -from datetime import timedelta +from datetime import datetime, timedelta, date +from collections import OrderedDict import base64 import re import os @@ -1812,7 +1813,7 @@ def callforpapers_edit(request, confname, sessionid): # on the same page. If feedback is still open, we show nothing feedback_fields = ('topic_importance', 'content_quality', 'speaker_knowledge', 'speaker_quality') if is_tester or not conference.feedbackopen: - feedbackdata = [{'key': k, 'title': k.replace('_', ' ').title(), 'num': [0] * 5} for k in feedback_fields] + feedbackdata = [{'key': k, 'title': k.replace('_', ' ').title(), 'score': OrderedDict(zip(range(1, 6), [0] * 5))} for k in feedback_fields] feedbacktext = [] fb = list(ConferenceSessionFeedback.objects.filter(conference=conference, session=session)) feedbackcount = len(fb) @@ -1820,7 +1821,7 @@ def callforpapers_edit(request, confname, sessionid): # Summarize the values for d in feedbackdata: if getattr(f, d['key']) > 0: - d['num'][getattr(f, d['key']) - 1] += 1 + d['score'][getattr(f, d['key'])] += 1 # Add the text if necessary if f.speaker_feedback: feedbacktext.append({ @@ -1838,7 +1839,17 @@ def callforpapers_edit(request, confname, sessionid): feedbackcomparisons.append({ 'key': measurement, 'title': measurement.replace('_', ' ').title(), - 'vals': curs.fetchall(), + 'data': [{ + 'label': '{}-{}'.format(r[0], r[1]), + 'value': r[2], + 'color': r[3] and 'red' or 'blue', + 'tooltip': '{} - {}{}\n\nCount: {}'.format( + r[0], + r[1], + ' (your score)' if r[3] else '', + r[2], + ), + } for r in curs.fetchall()], }) else: feedbackcount = 0 diff --git a/postgresqleu/util/templatetags/svgcharts.py b/postgresqleu/util/templatetags/svgcharts.py new file mode 100644 index 00000000..ef6bf397 --- /dev/null +++ b/postgresqleu/util/templatetags/svgcharts.py @@ -0,0 +1,117 @@ +from django import template +from django.utils.safestring import mark_safe + +import itertools +import math + +register = template.Library() + +defaultcolors = [ + '#3366CC', + '#DC3912', + '#FF9900', + '#109618', + '#990099', + '#3B3EAC', + '#0099C6', + '#DD4477', + '#66AA00', + '#B82E2E', + '#316395', + '#994499', + '#22AA99', + '#AAAA11', + '#6633CC', + '#E67300', + '#8B0707', + '#329262', + '#5574A6', + '#3B3EAC', +] + + +def _calculate_x(percent, radius): + # Minus 25% to turn the graph 90 degrees making it look better + return math.cos(2 * math.pi * (percent - 25) / 100) * radius + + +def _calculate_y(percent, radius): + # Minus 25% to turn the graph 90 degrees making it look better + return math.sin(2 * math.pi * (percent - 25) / 100) * radius + + +@register.simple_tag +def svgpiechart(svgdata, legendwidth=0): + radius = 100 + colors = itertools.cycle(defaultcolors) + + t = template.loader.get_template('util/svgpiechart.svg') + + slices = [] + currpercent = 0 + + total = sum(svgdata.values()) + + if total > 0: + for i, (k, v) in enumerate(svgdata.items()): + thispercent = 100 * v / total + slices.append({ + 'startx': _calculate_x(currpercent, radius), + 'starty': _calculate_y(currpercent, radius), + 'endx': _calculate_x(currpercent + thispercent, radius), + 'endy': _calculate_y(currpercent + thispercent, radius), + 'centerx': _calculate_x(currpercent + thispercent / 2, radius / 1.8), + 'centery': _calculate_y(currpercent + thispercent / 2, radius / 1.8), + 'percent': thispercent > 5 and round(thispercent, 1) or 0, + 'color': next(colors), + 'largearc': thispercent > 50 and 1 or 0, + 'drawslice': thispercent > 0, + 'popup': '{}\n\n{} ({}%)'.format(k, v, round(thispercent, 1)), + 'legend': { + 'y': -100 + (i + 1) * 20 - 10, + 'text': k, + }, + }) + currpercent += thispercent + + return t.render({ + 'radius': radius, + 'slices': slices, + 'legendwidth': legendwidth, + }) + + +@register.simple_tag +def svgbarchart(svgdata, legend=True, wratio=2): + t = template.loader.get_template('util/svgbarchart.svg') + + height = 100 + width = height * wratio + itemwidth = width // len(svgdata) + if legend: + grpahratio = 0.65 # Estimate that mostly works? + else: + graphratio = 1 - 10 / height + + maxval = max([d['value'] for d in svgdata]) + roundedmax = math.ceil(maxval / 2) * 2 + + for i, s in enumerate(svgdata): + s['leftpos'] = itemwidth * i + s['height'] = int((s['value'] / roundedmax) * graphratio * height) + s['negheight'] = -s['height'] + + return t.render({ + 'svgdata': svgdata, + 'height': height, + 'width': width, + 'bottom': graphratio * height, + 'legendheight': int((1 - graphratio) * height), + 'neglegendheight': int(-(1 - graphratio) * height), + 'itemwidth': itemwidth // 2, + 'negitemwidth': -1 * (itemwidth // 2), + 'gridlines': { + str(roundedmax // 2): int(graphratio * height / 2), + str(roundedmax): 0, + }, + }) diff --git a/template.jinja/confreg/session_feedback.html b/template.jinja/confreg/session_feedback.html index b7716e8d..ebce60f1 100644 --- a/template.jinja/confreg/session_feedback.html +++ b/template.jinja/confreg/session_feedback.html @@ -1,66 +1,8 @@ {%extends "base.html" %} {%block title%}Conference Session - {{conference}}{%endblock%} {%block extrahead%} - - - - + +{{ super() }} {%endblock%} {%block content%} @@ -123,6 +65,8 @@ The following feedback has been given on this presentation by

{%for f in feedbackdata%}
+
{{f.title}}
+{{svgcharts.svgpiechart(f.score, legendwidth=100)}}
{%endfor%} @@ -153,7 +97,9 @@ The following feedback has been given on this presentation by These charts show your average scores compared to the other sessions at this event.

{%for f in feedbackcomparisons%} -
+
+
{{f.title}}
+{{svgcharts.svgbarchart(f.data, wratio=2, legend=False)}}
{%endfor%} {%endif%} diff --git a/template/confreg/admin_conference_feedback.html b/template/confreg/admin_conference_feedback.html index 9fda18e2..af7b3e7b 100644 --- a/template/confreg/admin_conference_feedback.html +++ b/template/confreg/admin_conference_feedback.html @@ -1,44 +1,6 @@ {%extends "confreg/confadmin_base.html" %} +{%load svgcharts%} {%block title%}Conference Feedback - {{conference}}{%endblock%} -{%block extrahead%} - - - - -{%endblock%} {%block layoutblock%}

Conference Feedback - {{conference}}

@@ -57,7 +19,10 @@ A total of {{numresponses}} responses have been recorded.
{%for question in section.questions%} {%if question.graphdata%} -
+
+
{{question.question}}
+{% svgpiechart question.graphdata legendwidth=300 %} +
{%else%}

{{question.question}}

    diff --git a/template/util/svgbarchart.svg b/template/util/svgbarchart.svg new file mode 100644 index 00000000..12a24c9e --- /dev/null +++ b/template/util/svgbarchart.svg @@ -0,0 +1,16 @@ + + +{%for label, pos in gridlines.items %} + + {{label}} +{%endfor%} +{%for d in svgdata%} + + {{d.tooltip}} + +{%if legendheight and forloop.counter|divisibleby:2%} + {{d.label}} +{%endif%} +{%endfor%} + diff --git a/template/util/svgpiechart.svg b/template/util/svgpiechart.svg new file mode 100644 index 00000000..c8af3593 --- /dev/null +++ b/template/util/svgpiechart.svg @@ -0,0 +1,14 @@ + +{%for s in slices%}{%if s.drawslice%} + +{%if s.popup%}{{s.popup}}{%endif%} + +{%endif%}{%if s.legend and legendwidth %} + + {{s.legend.text}}{%endif%}{%endfor%} +{%for s in slices%} + {%if s.percent%}{{s.percent}}%{%if s.popup%}{{s.popup}}{%endif%}{%endif%} +{%endfor%} + -- 2.39.5