This removes the final dependency on google js apis.
In passing also make it possible to filter the conferences when picking
some, as that has become increasingly annoying as the number of
available conferences grow.
This also removes the non-linear trendlines support as that has never
really been useful anyway. Linear trendlines remain.
cursor: default;
}
+/* Time graphs */
+svg circle.point {
+ stroke-width: 0;
+ fill: transparent;
+}
+svg circle.point:hover {
+ stroke-width: 0.5;
+}
/*
* Sponsor admin
'form': form,
'title': report.title,
'ylabel': report.ylabel,
- 'headers': report.headers,
- 'graphdata': report.graphdata,
- 'maxpred': report.maxpred,
- 'trendlines': report.does_trendlines and trendlines or '',
- 'trendlines_supported': report.does_trendlines,
+ 'xlabel': 'Days',
+ 'series': report.series,
+ 'dayvals': report.dayvals,
+ 'trendlines': report.does_trendlines and trendlines,
'helplink': 'reports#time',
})
except ReportException as e:
self.title = title
self.ylabel = ylabel
self.conferences = conferences
- self.headers = None
- self.maxpred = 0
self.does_trendlines = True
self.curs = connection.cursor()
+ self.series = []
def run(self):
(maxday, minday) = self.maxmin()
if not maxday:
raise ReportException("There are no %s at this conference." % self.title.lower())
- allvals = [list(range(maxday, minday - 1, -1)), ]
- self.headers = ['Days']
- maxseen = 0
+ self.dayvals = list(range(maxday, minday - 1 if minday <= 0 else -1, -1))
for c in self.conferences:
myvals = [r[0] for r in self.fetch_all_data(c, minday, maxday)]
- allvals.append(myvals)
- self.headers.append(Header(c.conferencename))
- maxseen = max(max(myvals), maxseen)
-
- if maxday - minday:
- maxpred = float(maxseen) * maxday // (maxday - minday)
- else:
- maxpred = 10
- self.graphdata = list(zip(*allvals))
- self.maxpred = maxpred
+ self.series.append({
+ 'label': c.conferencename,
+ 'values': myvals,
+ })
class SingleConferenceReport(object):
def __init__(self, title, conferences):
self.title = title
self.ylabel = 'Number of registrations'
- self.maxpred = 0
self.does_trendlines = False
if len(conferences) != 1:
raise ReportException('For this report type you must pick a single conference')
self.conference = conferences[0]
- self.headers = None
self.curs = connection.cursor()
+ self.series = []
def maxmin(self):
self.curs.execute("SELECT max(startdate-payconfirmedat::date), min(startdate-payconfirmedat::date),max(startdate) FROM confreg_conferenceregistration r INNER JOIN confreg_conference c ON r.conference_id=c.id WHERE r.conference_id=%(id)s AND r.payconfirmedat IS NOT NULL", {
(maxday, minday, startdate) = self.maxmin()
if not maxday:
raise ReportException("There are no %s at this conference." % self.title.lower())
- allvals = [list(range(maxday, minday - 1, -1)), ]
- self.headers = ['Days']
+ self.dayvals = list(range(maxday, minday - 1, -1))
for header, rows in self.fetch_all_data(minday, maxday, startdate):
- allvals.append([r[0] for r in rows])
- self.headers.append(Header(header))
- self.graphdata = list(zip(*allvals))
+ self.series.append({
+ 'label': header,
+ 'values': [r[0] for r in rows],
+ })
# ##########################################################3
_trendlines = (
('', 'None'),
('linear', 'Linear'),
- ('exponential', 'Exponential'),
- ('polynomial', 'Polynomial'),
)
class TimeReportForm(forms.Form):
reporttype = forms.ChoiceField(required=True, choices=enumerate([r[0] for r in reporttypes], 1), label="Report type")
+ filter = forms.CharField(required=False, label="Filter", help_text="Type part of conference name to filter list")
conferences = GroupedModelMultipleChoiceField('series', required=True, queryset=Conference.objects.all().order_by('-startdate'))
trendline = forms.ChoiceField(required=False, choices=_trendlines)
if not self.user.is_superuser:
self.fields['conferences'].queryset = Conference.objects.filter(series__administrators=self.user)
+ self.fields['filter'].widget.attrs.update({
+ 'data-filter-select': 'id_conferences',
+ })
+
class QueuePartitionForm(forms.Form):
report = forms.CharField(required=True, widget=forms.HiddenInput())
from django import template
from django.utils.safestring import mark_safe
+import heapq
import itertools
import math
+import textwrap
register = template.Library()
str(roundedmax): 0,
},
})
+
+
+def _linreg(x, y):
+ # Simple linear regression
+ N = len(x)
+ Sx = Sy = Sxx = Syy = Sxy = 0.0
+ for x, y in zip(x, y):
+ Sx = Sx + x
+ Sy = Sy + y
+ Sxx = Sxx + x * x
+ Syy = Syy + y * y
+ Sxy = Sxy + x * y
+ det = Sxx * N - Sx * Sx
+ return (Sxy * N - Sy * Sx) / det, (Sxx * Sy - Sx * Sxy) / det
+
+
+@register.simple_tag
+def svglinechart(xlabels, series, wratio=3, ylabel='', xlabel='', alwayszeroline=False, trendlines=False):
+ colors = itertools.cycle(defaultcolors)
+
+ t = template.loader.get_template('util/svglinechart.svg')
+
+ width = 250
+ height = width // wratio
+
+ serieslen = len(xlabels)
+
+ maxval = 0
+ for s in series:
+ s['maxval'] = max(s['values'])
+ maxval = max(s['maxval'] for s in series)
+
+ # XXX: Make this 20 configurable!
+ roundingvalue = 20
+ numgridlines = 6
+
+ maxval = math.ceil(maxval / roundingvalue) * roundingvalue
+ gridvals = [(x + 1) * maxval // numgridlines for x in range(numgridlines)]
+ gridlines = [(v, height - int(height * v / maxval)) for v in gridvals]
+
+ xvals = [20 + x * 200 / (serieslen - 1) for x in range(serieslen)]
+ xgridvals = xlabels[::int(math.ceil(serieslen / 7))]
+ xgridlines = xvals[::int(math.ceil(serieslen / 7))]
+ xgridvals.append(xlabels[-1])
+ xgridlines.append(xvals[-1])
+ xgrid = zip(xgridvals, xgridlines)
+ zerolineat = None
+ if alwayszeroline and 0 not in xgridvals:
+ try:
+ ofs = xlabels.index(0)
+ xgridvals.append(xlabels[ofs])
+ xgridlines.append(xvals[ofs])
+ zerolineat = xvals[ofs]
+ except ValueError:
+ pass
+
+ for s in series:
+ s['values'] = list(zip(
+ xvals,
+ [height - int(height * v / maxval) for v in s['values']],
+ xlabels,
+ s['values'],
+ ))
+ s['color'] = next(colors)
+
+ legend = [{
+ 'label': textwrap.wrap(s['label'], 11)[:2],
+ 'color': s['color'],
+ 'ypos': i * 8 + 5,
+ } for i, s in enumerate(heapq.nlargest(8, series, key=lambda s: s['maxval']))]
+
+ # Trendlines will only be plotted against the first series
+ if trendlines:
+ a, b = _linreg([float(x[0]) for x in series[0]['values']], [float(y[1]) for y in series[0]['values']])
+ trendline = ((xvals[0], b), (xvals[-1], a * xvals[-1] + b))
+ else:
+ trendline = None
+
+ # X-wise:
+ # 10 pixels label
+ # 10 pixels scale
+ # ---- axis
+ # 200 pixels graph
+ # --- exis
+ # 30 pixels legend
+ # --> total width: 250
+
+ return t.render({
+ 'height': height,
+ 'halfheight': height / 2,
+ 'series': series,
+ 'ylabel': ylabel,
+ 'xlabel': xlabel,
+ 'gridlines': gridlines,
+ 'xgrid': xgrid,
+ 'zerolineat': zerolineat,
+ 'legend': legend,
+ 'trendline': trendline,
+ })
{%extends "confreg/confadmin_base.html" %}
{%load assets%}
+{%load svgcharts%}
{%block title%}Time based Reports{%endblock%}
{%block extrahead%}
-{%asset "css" "jqueryui1" %}
-{%asset "js" "jqueryui1" %}
-{%if graphdata%}
- <script type="text/javascript" src="https://www.google.com/jsapi"></script>
- <script type="text/javascript">
- google.load("visualization", "1", {packages:["corechart"]});
- google.setOnLoadCallback(drawChart);
- function drawChart() {
- var dataTable = new google.visualization.DataTable();
-{%for h in headers%}
- dataTable.addColumn('{%if forloop.isfirst%}string{%else%}number{%endif%}', '{{h}}');
- {%if h.hastoday%}
- dataTable.addColumn({type:'boolean', role:'certainty'});
- {%endif%}
-{%endfor%}
- dataTable.addRows([
-{%for r in graphdata%}[{{r|join:","}}],{%endfor%}
- ]);
-
- var options = {
- 'title': '{{title}}',
- 'hAxis': {
- 'direction': -1,
- 'title': 'Days before conference',
- 'minValue': 0,
- },
- 'vAxis': {
- 'title': '{{ylabel}}',
-{%if trendlines%}
- 'maxValue': {{maxpred}},
-{%endif%}
- },
-{%if trendlines%}
- 'trendlines': {
- 0: {
- 'type': '{{trendlines}}',
- 'visibleInLegend': true,
- 'opacity': 0.2,
- },
- }
-{%endif%}
- };
- var chart = new google.visualization.LineChart(document.getElementById('chart_div'));
- chart.draw(dataTable, options);
-
- // Make chart resizable
- $('#chart_div').resizable({
- stop: function(event, ui) {
- chart.draw(dataTable, options);
- },
- });
- }
-</script>
- {%endif%}
<style>
ul.errorlist {
color:red;
{%include "confreg/admin_backend_form_content.html" with savebutton="Generate report"%}
</form>
</div>
- <div class="row">
- <div id="chart_div" style="width: 100%; height: 500px;"></div>
- </div>
- <div class="row">
- Drag lower right corner to resize graph.
- </div>
-
+{%if series%}
+{% svglinechart dayvals series ylabel=ylabel xlabel=xlabel alwayszeroline=True trendlines=trendlines %}
+{%endif%}
{%endblock%}
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
+ viewBox="0 -5 250 {{height|add:20}}">
+ <line x1="20" x2="20" y1="0" y2="{{height}}" stroke="black" stroke-width="0.2" />
+ <line x1="220" x2="220" y1="0" y2="{{height}}" stroke="black" stroke-width="0.2" />
+ <line x1="20" x2="220" y1="{{height}}" y2="{{height}}" stroke="black" stroke-width="0.2" />
+{%if ylabel %}
+ <text x="5" y="{{halfheight}}" text-anchor="middle" transform="translate(-35,{{halfheight}}) rotate(270)" font-size="3pt" font-style="italic">{{ylabel}}</text>
+{%endif%}
+{%for g, y in gridlines %}
+ <line x1="20" x2="220" y1="{{y}}" y2="{{y}}" stroke-width="0.2" stroke="gray" stroke-dasharray="1"/>
+ <text x="18" y="{{y}}" text-anchor="end" dominant-baseline="middle" font-size="2pt">{{g}}</text>
+{%endfor%}
+ {%for s in series %}
+ <polyline points="{%for p in s.values%}{{p|slice:"0:2"|join:" "}} {%endfor%}" stroke-width="2" stroke="{{s.color}}" fill="none" style="vector-effect: non-scaling-stroke"/>
+ {%for x, y, xval, yval in s.values%}<circle cx="{{x}}" cy="{{y}}" r="1" stroke="{{s.color}}" class="point"><title>{{xlabel}}: {{xval}}
+{{s.label}}: {{yval}}</title></circle>{%endfor%}
+ {%endfor%}
+ {%for l in legend %}
+ <line x1="224" x2="228" y1="{{l.ypos}}" y2="{{l.ypos}}" stroke="{{l.color}}" stroke-width="0.5" />
+ <text x="230" y="{{l.ypos}}" dominant-baseline="middle" font-size="2pt">{%if l.label|length == 1 %}{{l.label.0}}{%else%}{%for ll in l.label%}<tspan x="230" dy="{%if forloop.counter0 == 0%}-0.5em{%else%}1em{%endif%}">{{ll}}</tspan>{%endfor%}{%endif%}</text>
+ {%endfor%}
+ {%for val, x in xgrid%}
+ <line x1="{{x}}" x2="{{x}}" y1="{{height}}" y2="{{height|add:2}}" stroke="black" stroke-width="0.2" />
+ <text x="{{x}}" y="{{height|add:3}}" dominant-baseline="hanging" text-anchor="middle" font-size="2pt">{{val}}</text>
+ {%endfor%}
+{%if zerolineat%}
+ <line x1="{{zerolineat}}" x2="{{zerolineat}}" y1="0" y2="{{height}}" stroke="black" stroke-width="0.2" />
+{%endif%}
+{%if trendline %}
+ <line x1="{{trendline.0.0}}" x2="{{trendline.1.0}}" y1="{{trendline.0.1}}" y2="{{trendline.1.1}}" stroke="{{series.0.color}}" stroke-width="0.2" stroke-opacity="0.5" />
+{%endif%}
+</svg>