diff --git a/ennstatus/donate/functions.py b/ennstatus/donate/functions.py --- a/ennstatus/donate/functions.py +++ b/ennstatus/donate/functions.py @@ -17,6 +17,19 @@ import os import os.path import csv +import statistics + +from collections import defaultdict + +from babel.numbers import parse_decimal + + +def load_csv_file(file): + + with open(file, encoding='utf-8', newline='') as csvfile: + csvreader = csv.reader(csvfile, delimiter=',') + rows = list(csvreader) + return rows def load_csv(date): @@ -24,11 +37,8 @@ def load_csv(date): filename = '.'.join([date, 'csv']) path = os.path.join('donations', filename) - with open(path, encoding='utf-8', newline='') as csvfile: - csvreader = csv.reader(csvfile, delimiter=',') - - for row in csvreader: - yield row + for row in load_csv_file(path): + yield row def get_choices(): @@ -39,3 +49,141 @@ def get_choices(): if not file.startswith('.') \ and file.endswith('.csv'): yield os.path.splitext(file)[0] + + +def get_all_csv(): + + for file in os.listdir('donations'): + path = os.path.join('donations', file) + yield load_csv_file(path) + + +def get_all_rows(): + + for csvfile in get_all_csv(): + for row in csvfile: + yield row + + +def all_csv_to_dict(): + + data = defaultdict(set) + + for row in get_all_rows(): + data[row[0]].add(parse_decimal(row[2], locale='de')) + return data + + +def data_to_year(data): + + year_data = defaultdict(set) + + for date, values in data.items(): + year = date.split('-')[0] + for value in values: + year_data[year].add(value) + + return year_data + + +def data_to_month(data): + + month_data = defaultdict(set) + + for date, values in data.items(): + month = date.rsplit('-', maxsplit=1)[0] + for value in values: + month_data[month].add(value) + + return month_data + + +def year_sum(data): + + year_data = data_to_year(data) + + for year, values in year_data.items(): + year_data[year] = sum(values) + + return year_data + + +def month_sum(data): + + month_data = data_to_month(data) + + for month, values in month_data.items(): + month_data[month] = sum(values) + + return month_data + + +def get_all_years_mean(data): + + mean = set() + year_data = year_sum(data) + + for year, values in year_data.items(): + mean.add(values) + + return statistics.mean(mean) + + +def get_months_mean(data): + + mean = set() + month_data = month_sum(data) + + for year, values in month_data.items(): + mean.add(values) + + return statistics.mean(mean) + + +def get_best_year(data): + + year_data = year_sum(data) + return max(year_data.items(), key=lambda x: x[1]) + + +def get_best_month(data): + + month_data = month_sum(data) + return max(month_data.items(), key=lambda x: x[1]) + + +def get_highest_donation(): + + data = all_csv_to_dict() + + for date, values in data.items(): + data[date] = max(values) + + return max( + sorted( + data.items(), + key=lambda x: x[0], + reverse=True + ), + key=lambda x: x[1] + ) + + +def get_median_donation(): + data = all_csv_to_dict() + donations = [] + + for value in data.values(): + donations.extend(value) + + return statistics.median(donations) + + +def get_mode_donation(): + data = all_csv_to_dict() + donations = [] + + for value in data.values(): + donations.extend(value) + + return statistics.mode(donations) diff --git a/ennstatus/donate/views.py b/ennstatus/donate/views.py --- a/ennstatus/donate/views.py +++ b/ennstatus/donate/views.py @@ -14,13 +14,27 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import json + from flask import (Blueprint, render_template, request, redirect, url_for, current_app) from babel.numbers import parse_decimal, format_decimal from ennstatus.donate.forms import DateForm -from ennstatus.donate.functions import load_csv, get_choices +from ennstatus.donate.functions import (load_csv, + get_choices, + all_csv_to_dict, + get_all_years_mean, + get_months_mean, + get_best_year, + get_best_month, + get_highest_donation, + get_median_donation, + get_mode_donation, + year_sum, + month_sum, + ) from ennstatus.root.forms import BPMForm from ennstatus.root.constants import BPM_ADDRESSES @@ -125,4 +139,66 @@ def received(): @donate_page.route('/statistics') def statistics(): - return render_template('donate/statistics.html') + data = all_csv_to_dict() + all_years_mean = format_decimal( + get_all_years_mean(data), + locale='de' + ) + month_mean = format_decimal( + get_months_mean(data), + locale='de' + ) + best_year = get_best_year(data) + best_year = (best_year[0], format_decimal(best_year[1], locale='de')) + + best_month = get_best_month(data) + best_month = (best_month[0], format_decimal(best_month[1], locale='de')) + + highest_donation = get_highest_donation() + highest_donation = (highest_donation[0], format_decimal(highest_donation[1], locale='de')) + + median_donation = format_decimal( + get_median_donation(), + locale='de' + ) + + mode_donation = format_decimal( + get_mode_donation(), + locale='de' + ) + + return render_template('donate/statistics.html', + all_years_mean=all_years_mean, + month_mean=month_mean, + best_year=best_year, + best_month=best_month, + highest_donation=highest_donation, + median_donation=median_donation, + mode_donation=mode_donation + ) + +@donate_page.route('/data/year') +def yeardata(): + data = all_csv_to_dict() + data = year_sum(data) + datalist = [] + + for key, values in data.items(): + datalist.append({"key": key, "value": float(values)}) + datalist.sort(key=lambda x: x["key"]) + + return json.dumps(datalist) + + +@donate_page.route('/data/month') +def monthdata(): + data = all_csv_to_dict() + data = month_sum(data) + data_list = [] + + for key, values in data.items(): + data_list.append({"key": key, "value": float(values)}) + + data_list.sort(key=lambda x: x["key"]) + + return json.dumps(data_list) diff --git a/ennstatus/static/css/barchart.css b/ennstatus/static/css/barchart.css new file mode 100644 --- /dev/null +++ b/ennstatus/static/css/barchart.css @@ -0,0 +1,47 @@ +.axis path, +.axis line { + fill: none; + stroke: black; + shape-rendering: crispEdges; +} +.axis text { + font-family: sans-serif; + font-size: 11px; +} +rect { + -moz-transition: all 0.3s; + -webkit-transition: all 0.3s; + -o-transition: all 0.3s; + transition: all 0.3s; +} +rect:hover{ + fill: orange; +} +.d3-tip { + line-height: 1; + font-weight: bold; + padding: 12px; + background: rgba(0, 0, 0, 0.8); + color: #fff; + border-radius: 2px; +} + +/* Creates a small triangle extender for the tooltip */ +.d3-tip:after { + box-sizing: border-box; + display: inline; + font-size: 10px; + width: 100%; + line-height: 1; + color: rgba(0, 0, 0, 0.8); + content: "\25BC"; + position: absolute; + text-align: center; +} + +/* Style northward tooltips differently */ +.d3-tip.n:after { + margin: -1px 0 0 0; + top: 100%; + left: 0; +} diff --git a/ennstatus/static/js/barcharts.js b/ennstatus/static/js/barcharts.js new file mode 100644 --- /dev/null +++ b/ennstatus/static/js/barcharts.js @@ -0,0 +1,152 @@ +d3.select(window).on("resize", throttle); +var ywidth = document.getElementById('yearchart').offsetWidth - 40 -30; +var yheight = (ywidth / 2) - 20 - 30; + +var mwidth = document.getElementById('monthchart').offsetWidth - 40 -30; +var mheight = (mwidth / 2) - 20 - 30; + +var yearchart, monthchart, tip; + +setup(ywidth, yheight, mwidth, mheight); + +function setup(ywidth, yheight, mwidth, mheight) { + yearchart = d3.select("#yearchart").append("svg") + .attr("width", ywidth) + .attr("height", yheight + 20 + 30) + .append("g") + .attr("transform", "translate(" + 40 + "," + 20 + ")"); + + monthchart = d3.select("#monthchart").append("svg") + .attr("width", mwidth) + .attr("height", mheight + 20 + 30) + .append("g") + .attr("transform", "translate(" + 40 + "," + 20 + ")"); + + tip = d3.tip() + .attr('class', 'd3-tip') + .offset([-10, 0]) + .html(function(d) { + return "" + d.key + ": " + d.value + ""; + }); + yearchart.call(tip); + monthchart.call(tip); + draw(ywidth, yheight, mwidth, mheight) +} + +function draw(ywidth, yheight, mwidth, mheight) { + d3.json("/donate/data/year", function(err, data) { + var xscale = d3.scale.ordinal().rangeRoundBands([0, ywidth], .1); + xscale.domain(data.map(function(d) { return d.key; })); + var yscale = d3.scale.linear().range([yheight, 0]); + yscale.domain([0, d3.max(data, function(d) { return d.value; })]); + var key = function(d) { return d.key; }; + + var xaxis = d3.svg.axis() + .scale(xscale) + .orient("bottom"); + var yaxis = d3.svg.axis() + .scale(yscale) + .orient("left"); + + yearchart.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0, " + yheight + ")") + .call(xaxis); + yearchart.append("g") + .attr("class", "y axis") + .call(yaxis) + .append("text") + .attr("transform", "rotate(-90)") + .attr("y", 6) + .attr("dy", ".71em") + .style("text-anchor", "end"); + + yearchart.selectAll("rect") + .data(data, key) + .enter() + .append("rect") + .attr("class", "bar") + .attr("x", function(d) { + return xscale(d.key); + }) + .attr("width", xscale.rangeBand()) + .attr("y", function(d) { + return yscale(d.value); + }) + .attr("height", function(d) { + return yheight - yscale(d.value); + }) + .attr("fill", function(d) { + return "#00ae18"; + }) + .on("mouseover", tip.show) + .on("mouseout", tip.hide) + + }); + d3.json("/donate/data/month", function(err, data) { + var xscale = d3.scale.ordinal().rangeRoundBands([0, mwidth], .1); + xscale.domain(data.map(function(d) { return d.key })); + var yscale = d3.scale.linear().range([mheight, 0]); + yscale.domain([0, d3.max(data, function(d) { return d.value; })]); + + var key = function(d) { return d.key; }; + var xaxis = d3.svg.axis() + .scale(xscale) + .orient("bottom"); + var yaxis = d3.svg.axis() + .scale(yscale) + .orient("left"); + + monthchart.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0, " + mheight + ")") + .call(xaxis) + monthchart.append("g") + .attr("class", "y axis") + .call(yaxis) + .append("text") + .attr("transform", "rotate(-90)") + .attr("y", 6) + .attr("dy", ".71em") + .style("text-anchor", "end"); + + monthchart.selectAll("rect") + .data(data, key) + .enter() + .append("rect") + .attr("class", "bar") + .attr("x", function(d) { + return xscale(d.key); + }) + .attr("width", xscale.rangeBand()) + .attr("y", function(d) { + return yscale(d.value); + }) + .attr("height", function(d) { + return mheight - yscale(d.value); + }) + .attr("fill", function(d) { + return "#00ae18"; + }) + .on("mouseover", tip.show) + .on("mouseout", tip.hide) + + }); +}; + +function redraw() { + ywidth = document.getElementById('yearchart').offsetWidth - 40 -30; + yheight = (ywidth / 2) - 20 - 30; + mwidth = document.getElementById('monthchart').offsetWidth - 40 -30; + mheight = (mwidth / 2) - 20 - 30; + d3.select('svg').remove(); + setup(ywidth, yheight, mwidth, mheight); +} + +var throttleTimer; +function throttle() { + window.clearTimeout(throttleTimer); + throttleTimer = window.setTimeout(function() { + redraw(); + }, 200); +} diff --git a/ennstatus/static/js/d3-tip/LICENSE b/ennstatus/static/js/d3-tip/LICENSE new file mode 100644 --- /dev/null +++ b/ennstatus/static/js/d3-tip/LICENSE @@ -0,0 +1,8 @@ +The MIT License (MIT) +Copyright (c) 2013 Justin Palmer + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/ennstatus/static/js/d3-tip/README.md b/ennstatus/static/js/d3-tip/README.md new file mode 100644 --- /dev/null +++ b/ennstatus/static/js/d3-tip/README.md @@ -0,0 +1,43 @@ +# d3.tip: Tooltips for d3.js visualizations + +[![](https://github-images.s3.amazonaws.com/skitch/Screen_Shot_2013-04-08_at_11.40.10_AM-20130408-114054.png)](http://bl.ocks.org/Caged/6476579) + +* [See a live demo](http://bl.ocks.org/Caged/6476579) +* [Example code](/examples) + +### API Docs +See the [API Documentation](docs/index.md) + +### Download Latest Version +* [Development Version](https://raw.github.com/Caged/d3-tip/master/index.js) : **6kb** / **~2kb gzipped** + +### Install with Bower +``` +bower install d3-tip +``` + +### Quick Usage +```javascript +/* Initialize tooltip */ +tip = d3.tip().attr('class', 'd3-tip').html(function(d) { return d; }); + +/* Invoke the tip in the context of your visualization */ +vis.call(tip) + +vis.selectAll('rect') + .data(data) +.enter().append('rect') + .attr('width', function() { return x.rangeBand() }) + .attr('height', function(d) { return h - y(d) }) + .attr('y', function(d) { return y(d) }) + .attr('x', function(d, i) { return x(i) }) + .on('mouseover', tip.show) + .on('mouseout', tip.hide) +``` + +If you want basic styling, you can include `example-styles.css` using a service like +rawgithub.com. + +```html + +``` diff --git a/ennstatus/static/js/d3-tip/index.js b/ennstatus/static/js/d3-tip/index.js new file mode 100644 --- /dev/null +++ b/ennstatus/static/js/d3-tip/index.js @@ -0,0 +1,324 @@ +// d3.tip +// Copyright (c) 2013 Justin Palmer +// +// Tooltips for d3.js SVG visualizations + +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module with d3 as a dependency. + define(['d3'], factory) + } else if (typeof module === 'object' && module.exports) { + // CommonJS + module.exports = function(d3) { + d3.tip = factory(d3) + return d3.tip + } + } else { + // Browser global. + root.d3.tip = factory(root.d3) + } +}(this, function (d3) { + + // Public - contructs a new tooltip + // + // Returns a tip + return function() { + var direction = d3_tip_direction, + offset = d3_tip_offset, + html = d3_tip_html, + node = initNode(), + svg = null, + point = null, + target = null + + function tip(vis) { + svg = getSVGNode(vis) + point = svg.createSVGPoint() + document.body.appendChild(node) + } + + // Public - show the tooltip on the screen + // + // Returns a tip + tip.show = function() { + var args = Array.prototype.slice.call(arguments) + if(args[args.length - 1] instanceof SVGElement) target = args.pop() + + var content = html.apply(this, args), + poffset = offset.apply(this, args), + dir = direction.apply(this, args), + nodel = getNodeEl(), + i = directions.length, + coords, + scrollTop = document.documentElement.scrollTop || document.body.scrollTop, + scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft + + nodel.html(content) + .style({ opacity: 1, 'pointer-events': 'all' }) + + while(i--) nodel.classed(directions[i], false) + coords = direction_callbacks.get(dir).apply(this) + nodel.classed(dir, true).style({ + top: (coords.top + poffset[0]) + scrollTop + 'px', + left: (coords.left + poffset[1]) + scrollLeft + 'px' + }) + + return tip + } + + // Public - hide the tooltip + // + // Returns a tip + tip.hide = function() { + var nodel = getNodeEl() + nodel.style({ opacity: 0, 'pointer-events': 'none' }) + return tip + } + + // Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value. + // + // n - name of the attribute + // v - value of the attribute + // + // Returns tip or attribute value + tip.attr = function(n, v) { + if (arguments.length < 2 && typeof n === 'string') { + return getNodeEl().attr(n) + } else { + var args = Array.prototype.slice.call(arguments) + d3.selection.prototype.attr.apply(getNodeEl(), args) + } + + return tip + } + + // Public: Proxy style calls to the d3 tip container. Sets or gets a style value. + // + // n - name of the property + // v - value of the property + // + // Returns tip or style property value + tip.style = function(n, v) { + if (arguments.length < 2 && typeof n === 'string') { + return getNodeEl().style(n) + } else { + var args = Array.prototype.slice.call(arguments) + d3.selection.prototype.style.apply(getNodeEl(), args) + } + + return tip + } + + // Public: Set or get the direction of the tooltip + // + // v - One of n(north), s(south), e(east), or w(west), nw(northwest), + // sw(southwest), ne(northeast) or se(southeast) + // + // Returns tip or direction + tip.direction = function(v) { + if (!arguments.length) return direction + direction = v == null ? v : d3.functor(v) + + return tip + } + + // Public: Sets or gets the offset of the tip + // + // v - Array of [x, y] offset + // + // Returns offset or + tip.offset = function(v) { + if (!arguments.length) return offset + offset = v == null ? v : d3.functor(v) + + return tip + } + + // Public: sets or gets the html value of the tooltip + // + // v - String value of the tip + // + // Returns html value or tip + tip.html = function(v) { + if (!arguments.length) return html + html = v == null ? v : d3.functor(v) + + return tip + } + + // Public: destroys the tooltip and removes it from the DOM + // + // Returns a tip + tip.destroy = function() { + if(node) { + getNodeEl().remove(); + node = null; + } + return tip; + } + + function d3_tip_direction() { return 'n' } + function d3_tip_offset() { return [0, 0] } + function d3_tip_html() { return ' ' } + + var direction_callbacks = d3.map({ + n: direction_n, + s: direction_s, + e: direction_e, + w: direction_w, + nw: direction_nw, + ne: direction_ne, + sw: direction_sw, + se: direction_se + }), + + directions = direction_callbacks.keys() + + function direction_n() { + var bbox = getScreenBBox() + return { + top: bbox.n.y - node.offsetHeight, + left: bbox.n.x - node.offsetWidth / 2 + } + } + + function direction_s() { + var bbox = getScreenBBox() + return { + top: bbox.s.y, + left: bbox.s.x - node.offsetWidth / 2 + } + } + + function direction_e() { + var bbox = getScreenBBox() + return { + top: bbox.e.y - node.offsetHeight / 2, + left: bbox.e.x + } + } + + function direction_w() { + var bbox = getScreenBBox() + return { + top: bbox.w.y - node.offsetHeight / 2, + left: bbox.w.x - node.offsetWidth + } + } + + function direction_nw() { + var bbox = getScreenBBox() + return { + top: bbox.nw.y - node.offsetHeight, + left: bbox.nw.x - node.offsetWidth + } + } + + function direction_ne() { + var bbox = getScreenBBox() + return { + top: bbox.ne.y - node.offsetHeight, + left: bbox.ne.x + } + } + + function direction_sw() { + var bbox = getScreenBBox() + return { + top: bbox.sw.y, + left: bbox.sw.x - node.offsetWidth + } + } + + function direction_se() { + var bbox = getScreenBBox() + return { + top: bbox.se.y, + left: bbox.e.x + } + } + + function initNode() { + var node = d3.select(document.createElement('div')) + node.style({ + position: 'absolute', + top: 0, + opacity: 0, + 'pointer-events': 'none', + 'box-sizing': 'border-box' + }) + + return node.node() + } + + function getSVGNode(el) { + el = el.node() + if(el.tagName.toLowerCase() === 'svg') + return el + + return el.ownerSVGElement + } + + function getNodeEl() { + if(node === null) { + node = initNode(); + // re-add node to DOM + document.body.appendChild(node); + }; + return d3.select(node); + } + + // Private - gets the screen coordinates of a shape + // + // Given a shape on the screen, will return an SVGPoint for the directions + // n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest), + // sw(southwest). + // + // +-+-+ + // | | + // + + + // | | + // +-+-+ + // + // Returns an Object {n, s, e, w, nw, sw, ne, se} + function getScreenBBox() { + var targetel = target || d3.event.target; + + while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) { + targetel = targetel.parentNode; + } + + var bbox = {}, + matrix = targetel.getScreenCTM(), + tbbox = targetel.getBBox(), + width = tbbox.width, + height = tbbox.height, + x = tbbox.x, + y = tbbox.y + + point.x = x + point.y = y + bbox.nw = point.matrixTransform(matrix) + point.x += width + bbox.ne = point.matrixTransform(matrix) + point.y += height + bbox.se = point.matrixTransform(matrix) + point.x -= width + bbox.sw = point.matrixTransform(matrix) + point.y -= height / 2 + bbox.w = point.matrixTransform(matrix) + point.x += width + bbox.e = point.matrixTransform(matrix) + point.x -= width / 2 + point.y -= height / 2 + bbox.n = point.matrixTransform(matrix) + point.y += height + bbox.s = point.matrixTransform(matrix) + + return bbox + } + + return tip + }; + +})); diff --git a/ennstatus/static/js/monthbar.js b/ennstatus/static/js/monthbar.js new file mode 100644 --- /dev/null +++ b/ennstatus/static/js/monthbar.js @@ -0,0 +1,90 @@ +d3.select(window).on("resize", mthrottle); +var width = document.getElementById('monthchart').offsetWidth - 40 -30; +var height = (width / 2) - 20 - 30; + +var svg, tip; + +msetup(width, height); + +function msetup(width, height) { + svg = d3.select("#monthchart").append("svg") + .attr("width", width) + .attr("height", height + 20 + 30) + .append("g") + .attr("transform", "translate(" + 40 + "," + 20 + ")"); + tip = d3.tip() + .attr('class', 'd3-tip') + .offset([-10, 0]) + .html(function(d) { + return "" + d.key + ": " + d.value + ""; + }); + svg.call(tip); + mdraw(width, height) +} + +function mdraw(width, height) { + d3.json("/donate/data/month", function(err, data) { + var xscale = d3.scale.ordinal().rangeRoundBands([0, width], .1); + xscale.domain(data.map(function(d) { return d.key; })); + var yscale = d3.scale.linear().range([height, 0]); + yscale.domain([0, d3.max(data, function(d) { return d.value; })]); + var key = function(d) { return d.key; }; + + var xaxis = d3.svg.axis() + .scale(xscale) + .orient("bottom"); + var yaxis = d3.svg.axis() + .scale(yscale) + .orient("left"); + + svg.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0, " + height + ")") + .call(xaxis); + svg.append("g") + .attr("class", "y axis") + .call(yaxis) + .append("text") + .attr("transform", "rotate(-90)") + .attr("y", 6) + .attr("dy", ".71em") + .style("text-anchor", "end"); + + svg.selectAll("rect") + .data(data, key) + .enter() + .append("rect") + .attr("class", "bar") + .attr("x", function(d) { + return xscale(d.key); + }) + .attr("width", xscale.rangeBand()) + .attr("y", function(d) { + return yscale(d.value); + }) + .attr("height", function(d) { + return height - yscale(d.value); + }) + .attr("fill", function(d) { + return "#00ae18"; + }) + .on("mouseover", tip.show) + .on("mouseout", tip.hide) + + }); +}; + +function mredraw() { + width = document.getElementById('monthchart').offsetWidth - 40 -30; + height = (width / 2) - 20 - 30; + d3.select('svg').remove(); + msetup(width, height); +} + +var throttleTimer; +function mthrottle() { + window.clearTimeout(throttleTimer); + throttleTimer = window.setTimeout(function() { + mredraw(); + }, 200); +} diff --git a/ennstatus/static/js/yearbar.js b/ennstatus/static/js/yearbar.js new file mode 100644 --- /dev/null +++ b/ennstatus/static/js/yearbar.js @@ -0,0 +1,90 @@ +d3.select(window).on("resize", throttle); +var width = document.getElementById('yearchart').offsetWidth - 40 -30; +var height = (width / 2) - 20 - 30; + +var svg, tip; + +setup(width, height); + +function setup(width, height) { + svg = d3.select("#yearchart").append("svg") + .attr("width", width) + .attr("height", height + 20 + 30) + .append("g") + .attr("transform", "translate(" + 40 + "," + 20 + ")"); + tip = d3.tip() + .attr('class', 'd3-tip') + .offset([-10, 0]) + .html(function(d) { + return "" + d.key + ": " + d.value + ""; + }); + svg.call(tip); + draw(width, height) +} + +function draw(width, height) { + d3.json("/donate/data/year", function(err, data) { + var xscale = d3.scale.ordinal().rangeRoundBands([0, width], .1); + xscale.domain(data.map(function(d) { return d.key; })); + var yscale = d3.scale.linear().range([height, 0]); + yscale.domain([0, d3.max(data, function(d) { return d.value; })]); + var key = function(d) { return d.key; }; + + var xaxis = d3.svg.axis() + .scale(xscale) + .orient("bottom"); + var yaxis = d3.svg.axis() + .scale(yscale) + .orient("left"); + + svg.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0, " + height + ")") + .call(xaxis); + svg.append("g") + .attr("class", "y axis") + .call(yaxis) + .append("text") + .attr("transform", "rotate(-90)") + .attr("y", 6) + .attr("dy", ".71em") + .style("text-anchor", "end"); + + svg.selectAll("rect") + .data(data, key) + .enter() + .append("rect") + .attr("class", "bar") + .attr("x", function(d) { + return xscale(d.key); + }) + .attr("width", xscale.rangeBand()) + .attr("y", function(d) { + return yscale(d.value); + }) + .attr("height", function(d) { + return height - yscale(d.value); + }) + .attr("fill", function(d) { + return "#00ae18"; + }) + .on("mouseover", tip.show) + .on("mouseout", tip.hide) + + }); +}; + +function redraw() { + width = document.getElementById('yearchart').offsetWidth - 40 -30; + height = (width / 2) - 20 - 30; + d3.select('#yearchart').select('svg').remove(); + setup(width, height); +} + +var throttleTimer; +function throttle() { + window.clearTimeout(throttleTimer); + throttleTimer = window.setTimeout(function() { + redraw(); + }, 200); +} diff --git a/ennstatus/static/js/yearchart.js b/ennstatus/static/js/yearchart.js new file mode 100644 --- /dev/null +++ b/ennstatus/static/js/yearchart.js @@ -0,0 +1,41 @@ +var width = document.getElementById('yearchart').offsetWidth; +var height = width / 2; + +setup(width, height); + +function setup(width, height) { + svg = d3.select("#yearchart").append("svg") + .attr("width", width) + .attr("height", height) + draw(width, height) +} + +function draw(width, height) { + d3.json("/donate/data/year", function(err, data) { + var xscale = d3.scale.ordinal() + .domain(d3.range(data.length)) + .rangeRoundBands([0, width], 0.05); + var yscale = d3.scale.linear() + .domain([0, d3.max(data, function(d) {return d.value;})]); + var key = function(d) { return d.key; }; + + svg.selectAll("rect") + .data(data, key) + .enter() + .append("rect") + .attr("x", function(d, i) { + return xscale(i); + }) + .attr("y", function(d, i) { + return height - yscale(d.value); + }) + .attr("width", xscale.rangeBand()) + .attr("height", function(d) { + return yscale(d.value); + }) + .attr("fill", function(d) { + return "rgb(0, 0, " + (d.value * 10) + ")"; + }); + + } +} diff --git a/ennstatus/templates/donate/statistics.html b/ennstatus/templates/donate/statistics.html --- a/ennstatus/templates/donate/statistics.html +++ b/ennstatus/templates/donate/statistics.html @@ -19,10 +19,54 @@ {% set title = "Donate - Statistics" %} +{% block styles %} + {{ super() }} + +{% endblock %} + {% block content %}

Donate Statistics

+
+
Mean per year:
+
{{ all_years_mean }}
+
Mean per month:
+
{{ month_mean }}
+
Best year:
+
{{ best_year[0] }} {{ best_year[1] }}
+
Best month:
+
{{ best_month[0] }} {{ best_month[1] }}
+
Highest donation:
+
{{ highest_donation[0] }} {{ highest_donation[1] }}
+
Median donation:
+
{{ median_donation }}
+
Mode donation:
+
{{ mode_donation }}
+
+
+
+
+
Donations per year
+
+
+
+
+
+
+
+
Donations per month
+
+
+
+
{% endblock %} + +{% block scripts %} + {{ super() }} + + + +{% endblock %}