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