From 78dd9860744035311ddd9678aec689be40850b33 Mon Sep 17 00:00:00 2001 From: Martin Keegan Date: Tue, 12 Mar 2013 13:34:29 +0000 Subject: [PATCH 01/28] factor out tree of if-statements --- pybossa/view/applications.py | 37 +++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/pybossa/view/applications.py b/pybossa/view/applications.py index c727287131..0875a33bdf 100644 --- a/pybossa/view/applications.py +++ b/pybossa/view/applications.py @@ -378,28 +378,31 @@ def import_task(short_name): dataurl = None csvform = BulkTaskCSVImportForm(request.form) gdform = BulkTaskGDImportForm(request.form) + if app.tasks or (request.args.get('template') or request.method == 'POST'): - if request.args.get('template') == 'image': - gdform.googledocs_url.data = \ - "https://docs.google.com/spreadsheet/ccc" \ - "?key=0AsNlt0WgPAHwdHFEN29mZUF0czJWMUhIejF6dWZXdkE" \ - "&usp=sharing" - elif request.args.get('template') == 'map': - gdform.googledocs_url.data = \ - "https://docs.google.com/spreadsheet/ccc" \ - "?key=0AsNlt0WgPAHwdGZnbjdwcnhKRVNlN1dGXy0tTnNWWXc" \ - "&usp=sharing" - elif request.args.get('template') == 'pdf': - gdform.googledocs_url.data = \ - "https://docs.google.com/spreadsheet/ccc" \ - "?key=0AsNlt0WgPAHwdEVVamc0R0hrcjlGdXRaUXlqRXlJMEE" \ - "&usp=sharing" - else: - pass + + googledocs_urls = [ + ('image', "https://docs.google.com/spreadsheet/ccc" \ + "?key=0AsNlt0WgPAHwdHFEN29mZUF0czJWMUhIejF6dWZXdkE" \ + "&usp=sharing"), + ('map', "https://docs.google.com/spreadsheet/ccc" \ + "?key=0AsNlt0WgPAHwdGZnbjdwcnhKRVNlN1dGXy0tTnNWWXc" \ + "&usp=sharing"), + ('pdf', "https://docs.google.com/spreadsheet/ccc" \ + "?key=0AsNlt0WgPAHwdEVVamc0R0hrcjlGdXRaUXlqRXlJMEE" \ + "&usp=sharing") + ] + + template = request.args.get('template') + for template_id, googledocs_url in googledocs_urls: + if template == template_id: + gdform.googledocs_url.data = googledocs_url + if 'csv_url' in request.form and csvform.validate_on_submit(): dataurl = csvform.csv_url.data elif 'googledocs_url' in request.form and gdform.validate_on_submit(): dataurl = ''.join([gdform.googledocs_url.data, '&output=csv']) + if dataurl: print "dataurl found" r = requests.get(dataurl) From 970a92ef2fd6c680c02d1fdfd1c07ad636cdbe22 Mon Sep 17 00:00:00 2001 From: Martin Keegan Date: Tue, 12 Mar 2013 13:39:19 +0000 Subject: [PATCH 02/28] respect original logic; be more efficient --- pybossa/view/applications.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pybossa/view/applications.py b/pybossa/view/applications.py index 0875a33bdf..74871db3ea 100644 --- a/pybossa/view/applications.py +++ b/pybossa/view/applications.py @@ -397,6 +397,7 @@ def import_task(short_name): for template_id, googledocs_url in googledocs_urls: if template == template_id: gdform.googledocs_url.data = googledocs_url + break if 'csv_url' in request.form and csvform.validate_on_submit(): dataurl = csvform.csv_url.data From 7c6cbfb3de9cc7e167e5c1390a8bbe42c719a485 Mon Sep 17 00:00:00 2001 From: Martin Keegan Date: Tue, 12 Mar 2013 13:43:01 +0000 Subject: [PATCH 03/28] move variables to be more local to code doing the work --- pybossa/view/applications.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pybossa/view/applications.py b/pybossa/view/applications.py index 74871db3ea..695191163c 100644 --- a/pybossa/view/applications.py +++ b/pybossa/view/applications.py @@ -421,15 +421,17 @@ def import_task(short_name): app=app, csvform=csvform, gdform=gdform) - empty = True + csvcontent = StringIO(r.text) csvreader = unicode_csv_reader(csvcontent) # TODO: check for errors - headers = [] - fields = set(['state', 'quorum', 'calibration', 'priority_0', - 'n_answers']) - field_header_index = [] try: + headers = [] + fields = set(['state', 'quorum', 'calibration', 'priority_0', + 'n_answers']) + field_header_index = [] + empty = True + for row in csvreader: if not headers: headers = row From 313a87c58bf2e1a17413509d1dcc5f13dece364d Mon Sep 17 00:00:00 2001 From: Martin Keegan Date: Tue, 12 Mar 2013 13:51:17 +0000 Subject: [PATCH 04/28] move task import loop into separate function, such that csvreader is parameterisable --- pybossa/view/applications.py | 62 +++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/pybossa/view/applications.py b/pybossa/view/applications.py index 695191163c..df7ec3b915 100644 --- a/pybossa/view/applications.py +++ b/pybossa/view/applications.py @@ -370,6 +370,38 @@ def settings(short_name): abort(404) +def import_tasks(app, csvreader): + headers = [] + fields = set(['state', 'quorum', 'calibration', 'priority_0', + 'n_answers']) + field_header_index = [] + empty = True + + for row in csvreader: + if not headers: + headers = row + if len(headers) != len(set(headers)): + raise CSVImportException('The file you uploaded has two headers with' + ' the same name.') + field_headers = set(headers) & fields + for field in field_headers: + field_header_index.append(headers.index(field)) + else: + info = {} + task = model.Task(app=app) + for idx, cell in enumerate(row): + if idx in field_header_index: + setattr(task, headers[idx], cell) + else: + info[headers[idx]] = cell + task.info = info + db.session.add(task) + db.session.commit() + empty = False + if empty: + raise CSVImportException('Oops! It looks like the file is empty.') + + @blueprint.route('//import', methods=['GET', 'POST']) def import_task(short_name): app = App.query.filter_by(short_name=short_name).first_or_404() @@ -426,35 +458,7 @@ def import_task(short_name): csvreader = unicode_csv_reader(csvcontent) # TODO: check for errors try: - headers = [] - fields = set(['state', 'quorum', 'calibration', 'priority_0', - 'n_answers']) - field_header_index = [] - empty = True - - for row in csvreader: - if not headers: - headers = row - if len(headers) != len(set(headers)): - raise CSVImportException('The file you uploaded has two headers with' - ' the same name.') - field_headers = set(headers) & fields - for field in field_headers: - field_header_index.append(headers.index(field)) - else: - info = {} - task = model.Task(app=app) - for idx, cell in enumerate(row): - if idx in field_header_index: - setattr(task, headers[idx], cell) - else: - info[headers[idx]] = cell - task.info = info - db.session.add(task) - db.session.commit() - empty = False - if empty: - raise CSVImportException('Oops! It looks like the file is empty.') + import_tasks(app, csvreader) flash('Tasks imported successfully!', 'success') return redirect(url_for('.details', short_name=app.short_name)) except CSVImportException, err_msg: From 26ec3d78f991e8da7fae59ebcbc54ae3eab39ce6 Mon Sep 17 00:00:00 2001 From: Martin Keegan Date: Tue, 12 Mar 2013 13:53:55 +0000 Subject: [PATCH 05/28] factor all the error paths back together --- pybossa/view/applications.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pybossa/view/applications.py b/pybossa/view/applications.py index df7ec3b915..502e8d6c55 100644 --- a/pybossa/view/applications.py +++ b/pybossa/view/applications.py @@ -438,26 +438,19 @@ def import_task(short_name): if dataurl: print "dataurl found" - r = requests.get(dataurl) try: + r = requests.get(dataurl) if r.status_code == 403: raise CSVImportException("Oops! It looks like you don't have permission to access" " that file!", 'error') if (not 'text/plain' in r.headers['content-type'] and not 'text/csv' in r.headers['content-type']): raise CSVImportException("Oops! That file doesn't look like the right file.", 'error') - except CSVImportException, err_msg: - flash(err_msg, 'error') - return render_template('/applications/import.html', - title=title, - app=app, - csvform=csvform, - gdform=gdform) - csvcontent = StringIO(r.text) - csvreader = unicode_csv_reader(csvcontent) - # TODO: check for errors - try: + csvcontent = StringIO(r.text) + csvreader = unicode_csv_reader(csvcontent) + + # TODO: check for errors import_tasks(app, csvreader) flash('Tasks imported successfully!', 'success') return redirect(url_for('.details', short_name=app.short_name)) From 8699bfe327db929322ada1e9fd289d6f282903fd Mon Sep 17 00:00:00 2001 From: Martin Keegan Date: Wed, 13 Mar 2013 17:57:54 +0000 Subject: [PATCH 06/28] use dict --- pybossa/view/applications.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/pybossa/view/applications.py b/pybossa/view/applications.py index 502e8d6c55..d1a45b029e 100644 --- a/pybossa/view/applications.py +++ b/pybossa/view/applications.py @@ -413,23 +413,21 @@ def import_task(short_name): if app.tasks or (request.args.get('template') or request.method == 'POST'): - googledocs_urls = [ - ('image', "https://docs.google.com/spreadsheet/ccc" \ - "?key=0AsNlt0WgPAHwdHFEN29mZUF0czJWMUhIejF6dWZXdkE" \ - "&usp=sharing"), - ('map', "https://docs.google.com/spreadsheet/ccc" \ - "?key=0AsNlt0WgPAHwdGZnbjdwcnhKRVNlN1dGXy0tTnNWWXc" \ - "&usp=sharing"), - ('pdf', "https://docs.google.com/spreadsheet/ccc" \ + googledocs_urls = { + 'image': "https://docs.google.com/spreadsheet/ccc" \ + "?key=0AsNlt0WgPAHwdHFEN29mZUF0czJWMUhIejF6dWZXdkE" \ + "&usp=sharing", + 'map': "https://docs.google.com/spreadsheet/ccc" \ + "?key=0AsNlt0WgPAHwdGZnbjdwcnhKRVNlN1dGXy0tTnNWWXc" \ + "&usp=sharing", + 'pdf': "https://docs.google.com/spreadsheet/ccc" \ "?key=0AsNlt0WgPAHwdEVVamc0R0hrcjlGdXRaUXlqRXlJMEE" \ - "&usp=sharing") - ] + "&usp=sharing" + } template = request.args.get('template') - for template_id, googledocs_url in googledocs_urls: - if template == template_id: - gdform.googledocs_url.data = googledocs_url - break + if template in googledocs_urls: + gdform.googledocs_url.data = googledocs_urls[template] if 'csv_url' in request.form and csvform.validate_on_submit(): dataurl = csvform.csv_url.data From 5c5f1358fc11855db6f8edb93d9f8e8e32d3453f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lombra=C3=B1a=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2013 08:42:14 +0100 Subject: [PATCH 07/28] Specify the required PostgreSQL minimum version --- doc/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install.rst b/doc/install.rst index ecb99c3010..1b8db13ea9 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -7,7 +7,7 @@ PyBossa is a python web application built using the Flask micro-framework. Pre-requisites: * Python >= 2.6, <3.0 - * A database plus Python bindings for PostgreSQL + * A database plus Python bindings for PostgreSQL version 9.1 * pip for installing python packages (e.g. on ubuntu python-pip) .. note:: From 8972678818d0a384a56eeb272795781a79dcede6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lombra=C3=B1a=20Gonz=C3=A1lez?= Date: Wed, 13 Mar 2013 11:54:34 +0100 Subject: [PATCH 08/28] user, dates and hour stats for an app --- pybossa/cache/apps.py | 172 ++++++++++++++++++++++++++++++++++- pybossa/view/applications.py | 11 +++ 2 files changed, 182 insertions(+), 1 deletion(-) diff --git a/pybossa/cache/apps.py b/pybossa/cache/apps.py index c0fdb98b4c..04cf290bf1 100644 --- a/pybossa/cache/apps.py +++ b/pybossa/cache/apps.py @@ -15,10 +15,15 @@ from sqlalchemy.sql import func, text from pybossa.core import cache from pybossa.core import db -from pybossa.model import Featured, App, TaskRun +from pybossa.model import Featured, App, TaskRun, Task from pybossa.util import pretty_date import json +import string +import operator +import datetime + +STATS_TIMEOUT=50 @cache.cached(key_prefix="front_page_featured_apps") def get_featured_front_page(): @@ -224,6 +229,171 @@ def get_draft(page=1, per_page=5): return apps, count +@cache.memoize(timeout=STATS_TIMEOUT) +def get_task_runs(app_id): + """Return all the Task Runs for a given app_id""" + task_runs = db.session.query(TaskRun).filter_by(app_id=app_id).all() + return task_runs + + +@cache.memoize(timeout=STATS_TIMEOUT) +def get_tasks(app_id): + """Return all the tasks for a given app_id""" + tasks = db.session.query(Task).filter_by(app_id=app_id).all() + return tasks + + +@cache.memoize(timeout=STATS_TIMEOUT) +def stats_users(app_id): + """Return users's stats for a given app_id""" + task_runs = get_task_runs(app_id) + users = [] + auth_users = [] + anon_users = [] + for tr in task_runs: + if (tr.user_id is None): + users.append(-1) + anon_users.append(tr.user_ip) + else: + users.append(tr.user_id) + auth_users.append(tr.user_id) + return users, anon_users, auth_users + + +@cache.memoize(timeout=STATS_TIMEOUT) +def stats_dates(app_id): + dates = {} + dates_anon = {} + dates_auth = {} + dates_n_tasks = {} + dates_estimate = {} + + n_answers_per_task = [] + avg = 0 + + tasks = get_tasks(app_id) + task_runs = get_task_runs(app_id) + + for t in tasks: + n_answers_per_task.append(t.n_answers) + avg = sum(n_answers_per_task)/len(tasks) + total_n_tasks = len(tasks) + + for tr in task_runs: + # Data for dates + date, hour = string.split(tr.finish_time, "T") + tr.finish_time = string.split(tr.finish_time, '.')[0] + hour = string.split(hour,":")[0] + + # Dates + if date in dates.keys(): + dates[date] +=1 + else: + dates[date] = 1 + + if date in dates_n_tasks.keys(): + dates_n_tasks[date] = total_n_tasks * avg + else: + dates_n_tasks[date] = total_n_tasks * avg + + if tr.user_id is None: + if date in dates_anon.keys(): + dates_anon[date] += 1 + else: + dates_anon[date] = 1 + else: + if date in dates_auth.keys(): + dates_auth[date] += 1 + else: + dates_auth[date] = 1 + return dates, dates_anon, dates_auth + +@cache.memoize(timeout=STATS_TIMEOUT) +def stats_hours(app_id): + hours = {} + hours_anon = {} + hours_auth = {} + max_hours = 0 + max_hours_anon = 0 + max_hours_auth = 0 + + + tasks = get_tasks(app_id) + task_runs = get_task_runs(app_id) + + # initialize hours keys + for i in range(0,24): + hours[u'%s' % i]=0 + hours_anon[u'%s' % i]=0 + hours_auth[u'%s' % i]=0 + + for tr in task_runs: + # Hours + date, hour = string.split(tr.finish_time, "T") + tr.finish_time = string.split(tr.finish_time, '.')[0] + hour = string.split(hour,":")[0] + + if hour in hours.keys(): + hours[hour] += 1 + if (hours[hour] > max_hours): + max_hours = hours[hour] + + if tr.user_id is None: + if hour in hours_anon.keys(): + hours_anon[hour] += 1 + if (hours_anon[hour] > max_hours_anon): + max_hours_anon = hours_anon[hour] + + else: + if hour in hours_auth.keys(): + hours_auth[hour] += 1 + if (hours_auth[hour] > max_hours_auth): + max_hours_auth = hours_auth[hour] + return hours, hours_anon, hours_auth, + + +@cache.memoize(timeout=STATS_TIMEOUT) +def stats_summary(app_id): + """Prints a small stats summary for the given app""" + tasks = get_tasks(app_id) + hours, hours_anon, hours_auth = stats_hours(app_id) + users, anon_users, auth_users = stats_users(app_id) + dates, dates_anon, dates_auth = stats_dates(app_id) + + n_answers_per_task = [] + for t in tasks: + n_answers_per_task.append(t.n_answers) + avg = sum(n_answers_per_task)/len(tasks) + total_n_tasks = len(tasks) + + print "total days used: %s" % len(dates) + sorted_answers = sorted(dates.iteritems(), key=operator.itemgetter(0)) + if len(sorted_answers) > 0: + last_day = datetime.datetime.strptime( sorted_answers[-1][0], "%Y-%m-%d") + print last_day + total_answers = sum(dates.values()) + if len(dates) > 0: + avg_answers_per_day = total_answers/len(dates) + required_days_to_finish = ((avg*total_n_tasks)-total_answers)/avg_answers_per_day + print "total number of required answers: %s" % (avg*total_n_tasks) + print "total number of received answers: %s" % total_answers + print "avg number of answers per day: %s" % avg_answers_per_day + print "To complete all the tasks at a pace of %s per day, the app will need %s days" % (avg_answers_per_day, required_days_to_finish) + + + + + + + + + + + + + + + def reset(): """Clean the cache""" cache.delete('front_page_featured_apps') diff --git a/pybossa/view/applications.py b/pybossa/view/applications.py index d1a45b029e..30aacbeec3 100644 --- a/pybossa/view/applications.py +++ b/pybossa/view/applications.py @@ -785,3 +785,14 @@ def get_csv_task_run(): return render_template('/applications/export.html', title=title, app=app) + +@blueprint.route('//stats') +def stats(short_name): + """Returns App Stats""" + app = db.session.query(model.App).filter_by(short_name=short_name).first() + title="Application: %s · Stats" % app.name + cached_apps.stats_summary(app.id) + return render_template('/applications/stats.html', + title=title, + userStats=None, + app=app) From ce27e6fc76276564e97097dc49a08b19fcffe316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lombra=C3=B1a=20Gonz=C3=A1lez?= Date: Wed, 13 Mar 2013 12:50:24 +0100 Subject: [PATCH 09/28] Generate user, dates, hours JSON objects for the stats page --- pybossa/cache/apps.py | 217 ++++++++++++++++++++++++++++++++++- pybossa/view/applications.py | 5 +- 2 files changed, 218 insertions(+), 4 deletions(-) diff --git a/pybossa/cache/apps.py b/pybossa/cache/apps.py index 04cf290bf1..551b0d882c 100644 --- a/pybossa/cache/apps.py +++ b/pybossa/cache/apps.py @@ -22,6 +22,8 @@ import string import operator import datetime +import time +from datetime import timedelta STATS_TIMEOUT=50 @@ -306,7 +308,7 @@ def stats_dates(app_id): dates_auth[date] += 1 else: dates_auth[date] = 1 - return dates, dates_anon, dates_auth + return dates, dates_n_tasks, dates_anon, dates_auth @cache.memoize(timeout=STATS_TIMEOUT) def stats_hours(app_id): @@ -349,7 +351,7 @@ def stats_hours(app_id): hours_auth[hour] += 1 if (hours_auth[hour] > max_hours_auth): max_hours_auth = hours_auth[hour] - return hours, hours_anon, hours_auth, + return hours, hours_anon, hours_auth, max_hours, max_hours_anon, max_hours_auth @cache.memoize(timeout=STATS_TIMEOUT) @@ -358,7 +360,7 @@ def stats_summary(app_id): tasks = get_tasks(app_id) hours, hours_anon, hours_auth = stats_hours(app_id) users, anon_users, auth_users = stats_users(app_id) - dates, dates_anon, dates_auth = stats_dates(app_id) + dates, dates_n_tasks, dates_anon, dates_auth = stats_dates(app_id) n_answers_per_task = [] for t in tasks: @@ -381,17 +383,226 @@ def stats_summary(app_id): print "To complete all the tasks at a pace of %s per day, the app will need %s days" % (avg_answers_per_day, required_days_to_finish) +@cache.memoize(timeout=STATS_TIMEOUT) +def stats_format_dates(app_id, dates, dates_n_tasks, dates_estimate, + dates_anon, dates_auth): + """Format dates stats into a JSON format""" + dayNewStats = dict(label="Anon + Auth", values=[]) + dayAvgAnswers = dict(label="Expected Answers", values=[]) + dayEstimates = dict(label="Estimation", values=[]) + dayTotalStats = dict(label="Total", disabled="True", values=[]) + dayNewAnonStats = dict(label="Anonymous", values=[]) + dayNewAuthStats = dict(label="Authenticated", values=[]) + + total = 0 + for d in sorted(dates.keys()): + # JavaScript expects miliseconds since EPOCH + # New answers per day + dayNewStats['values'].append( + [int( + time.mktime(time.strptime( d, "%Y-%m-%d"))*1000 + ), + dates[d]]) + + dayAvgAnswers['values'].append( + [int( + time.mktime(time.strptime( d, "%Y-%m-%d"))*1000 + ), + dates_n_tasks[d]]) + + # Total answers per day + total = total + dates[d] + dayTotalStats['values'].append( + [int( + time.mktime(time.strptime( d, "%Y-%m-%d"))*1000 + ), + total]) + + # Anonymous answers per day + if d in (dates_anon.keys()): + dayNewAnonStats['values'].append( + [int( + time.mktime(time.strptime( d, "%Y-%m-%d"))*1000 + ), + dates_anon[d]]) + else: + dayNewAnonStats['values'].append( + [int( + time.mktime(time.strptime( d, "%Y-%m-%d"))*1000 + ), + 0]) + + # Authenticated answers per day + if d in (dates_auth.keys()): + dayNewAuthStats['values'].append( + [int( + time.mktime(time.strptime( d, "%Y-%m-%d"))*1000 + ), + dates_auth[d]]) + else: + dayNewAuthStats['values'].append( + [int( + time.mktime(time.strptime( d, "%Y-%m-%d"))*1000 + ), + 0]) + for d in sorted(dates_estimate.keys()): + dayEstimates['values'].append( + [int( + time.mktime(time.strptime( d, "%Y-%m-%d"))*1000 + ), + dates_estimate[d]]) + dayAvgAnswers['values'].append( + [int( + time.mktime(time.strptime( d, "%Y-%m-%d"))*1000 + ), + dates_n_tasks.values()[0]]) + return dayNewStats, dayAvgAnswers, dayEstimates, dayTotalStats, \ + dayNewAnonStats, dayNewAuthStats +@cache.memoize(timeout=STATS_TIMEOUT) +def stats_format_hours(app_id, hours, hours_anon, hours_auth, + max_hours, max_hours_anon, max_hours_auth): + """Format hours stats into a JSON format""" + hourNewStats = dict(label="Anon + Auth", disabled="True", values=[], max=0) + hourNewAnonStats = dict(label="Anonymous", values=[], max=0) + hourNewAuthStats = dict(label="Authenticated", values=[], max=0) + + hourNewStats['max'] = max_hours + hourNewAnonStats['max'] = max_hours_anon + hourNewAuthStats['max'] = max_hours_auth + + for h in sorted(hours.keys()): + # New answers per hour + #hourNewStats['values'].append(dict(x=int(h), y=hours[h], size=hours[h]*10)) + if (hours[h] != 0): + hourNewStats['values'].append([int(h), hours[h], (hours[h]*5)/max_hours]) + else: + hourNewStats['values'].append([int(h), hours[h], 0]) + + # New Anonymous answers per hour + if h in hours_anon.keys(): + #hourNewAnonStats['values'].append(dict(x=int(h), y=hours[h], size=hours_anon[h]*10)) + if (hours_anon[h] != 0): + hourNewAnonStats['values'].append([int(h), hours_anon[h], (hours_anon[h]*5)/max_hours]) + else: + hourNewAnonStats['values'].append([int(h), hours_anon[h],0 ]) + + # New Authenticated answers per hour + if h in hours_auth.keys(): + #hourNewAuthStats['values'].append(dict(x=int(h), y=hours[h], size=hours_auth[h]*10)) + if (hours_auth[h] != 0): + hourNewAuthStats['values'].append([int(h), hours_auth[h], (hours_auth[h]*5)/max_hours]) + else: + hourNewAuthStats['values'].append([int(h), hours_auth[h], 0]) + return hourNewStats, hourNewAnonStats, hourNewAuthStats +@cache.memoize(timeout=STATS_TIMEOUT) +def stats_format_users(app_id, users, anon_users, auth_users): + """Format User Stats into JSON""" + userStats = dict(label="User Statistics", values=[]) + userAnonStats = dict(label="Anonymous Users", values=[], top5=[], locs=[]) + userAuthStats = dict(label="Authenticated Users", values=[], top5=[]) + + # Count total number of answers for users + anonymous = 0 + authenticated = 0 + for e in users: + if e == -1: + anonymous += 1 + else: + authenticated += 1 + + userStats['values'].append(dict(label="Anonymous", value=[0, anonymous])) + userStats['values'].append(dict(label="Authenticated", value=[0, authenticated])) + from collections import Counter + c_anon_users = Counter(anon_users) + c_auth_users = Counter(auth_users) + + for u in list(c_anon_users): + userAnonStats['values']\ + .append(dict(label=u, value=c_anon_users[u])) + + for u in list(c_auth_users): + userAuthStats['values']\ + .append(dict(label=u, value=c_auth_users[u])) + + # Get location for Anonymous users + import pygeoip + gi = pygeoip.GeoIP('dat/GeoIP.dat') + gic = pygeoip.GeoIP('dat/GeoLiteCity.dat') + top5_anon = [] + top5_auth = [] + loc_anon = [] + for u in c_anon_users.most_common(5): + loc = gic.record_by_addr(u[0]) + if (len(loc.keys()) == 0): + loc['latitude'] = 0 + loc['longitude'] = 0 + top5_anon.append(dict(ip=u[0],loc=loc, tasks=u[1])) + + for u in c_anon_users.items(): + loc = gic.record_by_addr(u[0]) + if (len(loc.keys()) == 0): + loc['latitude'] = 0 + loc['longitude'] = 0 + loc_anon.append(dict(ip=u[0],loc=loc, tasks=u[1])) + + for u in c_auth_users.most_common(5): + top5_auth.append(dict(id=u[0], tasks=u[1])) + + userAnonStats['top5'] = top5_anon + userAnonStats['locs'] = loc_anon + userAuthStats['top5'] = top5_auth + + return userStats, userAnonStats, userAuthStats + + +@cache.memoize(timeout=STATS_TIMEOUT) +def get_stats(app_id): + """Return the stats a given app""" + tasks = get_tasks(app_id) + hours, hours_anon, hours_auth, max_hours, \ + max_hours_anon, max_hours_auth = stats_hours(app_id) + users, anon_users, auth_users = stats_users(app_id) + dates, dates_n_tasks, dates_anon, dates_auth = stats_dates(app_id) + + n_answers_per_task = [] + for t in tasks: + n_answers_per_task.append(t.n_answers) + avg = sum(n_answers_per_task)/len(tasks) + total_n_tasks = len(tasks) + + sorted_answers = sorted(dates.iteritems(), key=operator.itemgetter(0)) + if len(sorted_answers) > 0: + last_day = datetime.datetime.strptime( sorted_answers[-1][0], "%Y-%m-%d") + total_answers = sum(dates.values()) + if len(dates) > 0: + avg_answers_per_day = total_answers/len(dates) + required_days_to_finish = ((avg*total_n_tasks)-total_answers)/avg_answers_per_day + + pace = total_answers + + dates_estimate = {} + for i in range(0, required_days_to_finish + 2): + tmp = last_day + timedelta(days=(i)) + tmp_str = tmp.date().strftime('%Y-%m-%d') + dates_estimate[tmp_str] = pace + pace = pace + avg_answers_per_day + dates_stats = stats_format_dates(app_id, dates, dates_n_tasks, dates_estimate, + dates_anon, dates_auth) + hours_stats = stats_format_hours(app_id, hours, hours_anon, hours_auth, + max_hours, max_hours_anon, max_hours_auth) + users_stats = stats_format_users(app_id, users, anon_users, auth_users) + return dates_stats, hours_stats, users_stats def reset(): diff --git a/pybossa/view/applications.py b/pybossa/view/applications.py index 30aacbeec3..4511bbc8eb 100644 --- a/pybossa/view/applications.py +++ b/pybossa/view/applications.py @@ -791,7 +791,10 @@ def stats(short_name): """Returns App Stats""" app = db.session.query(model.App).filter_by(short_name=short_name).first() title="Application: %s · Stats" % app.name - cached_apps.stats_summary(app.id) + dates_stats, hours_stats, users_stats = cached_apps.get_stats(app.id) + print dates_stats + print hours_stats + print users_stats return render_template('/applications/stats.html', title=title, userStats=None, From ad7c8c5a75ac06d97cf38b1643278ff2c3ad1257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lombra=C3=B1a=20Gonz=C3=A1lez?= Date: Wed, 13 Mar 2013 14:08:48 +0100 Subject: [PATCH 10/28] Default Stats page per app built --- pybossa/cache/apps.py | 8 +- .../css/stats/MarkerCluster.Default.css | 38 ++ pybossa/static/css/stats/MarkerCluster.css | 6 + pybossa/static/css/stats/stats.css | 99 +++++ pybossa/static/img/info-icon.png | Bin 0 -> 1229 bytes pybossa/static/js/stats/flotr2.min.js | 27 ++ .../static/js/stats/leaflet.markercluster.js | 6 + pybossa/templates/applications/stats.html | 411 ++++++++++++++++++ pybossa/view/applications.py | 27 +- 9 files changed, 615 insertions(+), 7 deletions(-) create mode 100644 pybossa/static/css/stats/MarkerCluster.Default.css create mode 100644 pybossa/static/css/stats/MarkerCluster.css create mode 100644 pybossa/static/css/stats/stats.css create mode 100644 pybossa/static/img/info-icon.png create mode 100644 pybossa/static/js/stats/flotr2.min.js create mode 100644 pybossa/static/js/stats/leaflet.markercluster.js create mode 100644 pybossa/templates/applications/stats.html diff --git a/pybossa/cache/apps.py b/pybossa/cache/apps.py index 551b0d882c..0f438dfb8a 100644 --- a/pybossa/cache/apps.py +++ b/pybossa/cache/apps.py @@ -460,8 +460,8 @@ def stats_format_dates(app_id, dates, dates_n_tasks, dates_estimate, dates_n_tasks.values()[0]]) - return dayNewStats, dayAvgAnswers, dayEstimates, dayTotalStats, \ - dayNewAnonStats, dayNewAuthStats + return dayNewStats, dayNewAnonStats, dayNewAuthStats, \ + dayTotalStats, dayAvgAnswers, dayEstimates @cache.memoize(timeout=STATS_TIMEOUT) @@ -560,7 +560,8 @@ def stats_format_users(app_id, users, anon_users, auth_users): userAnonStats['locs'] = loc_anon userAuthStats['top5'] = top5_auth - return userStats, userAnonStats, userAuthStats + return dict(users=userStats, anon=userAnonStats, auth=userAuthStats, + n_anon=anonymous, n_auth=authenticated) @cache.memoize(timeout=STATS_TIMEOUT) @@ -602,6 +603,7 @@ def get_stats(app_id): max_hours, max_hours_anon, max_hours_auth) users_stats = stats_format_users(app_id, users, anon_users, auth_users) + print users_stats['n_anon'] return dates_stats, hours_stats, users_stats diff --git a/pybossa/static/css/stats/MarkerCluster.Default.css b/pybossa/static/css/stats/MarkerCluster.Default.css new file mode 100644 index 0000000000..90558dd6c3 --- /dev/null +++ b/pybossa/static/css/stats/MarkerCluster.Default.css @@ -0,0 +1,38 @@ +.marker-cluster-small { + background-color: rgba(181, 226, 140, 0.6); + } +.marker-cluster-small div { + background-color: rgba(110, 204, 57, 0.6); + } + +.marker-cluster-medium { + background-color: rgba(241, 211, 87, 0.6); + } +.marker-cluster-medium div { + background-color: rgba(240, 194, 12, 0.6); + } + +.marker-cluster-large { + background-color: rgba(253, 156, 115, 0.6); + } +.marker-cluster-large div { + background-color: rgba(241, 128, 23, 0.6); + } + +.marker-cluster { + background-clip: padding-box; + border-radius: 20px; + } +.marker-cluster div { + width: 30px; + height: 30px; + margin-left: 5px; + margin-top: 5px; + + text-align: center; + border-radius: 15px; + font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; + } +.marker-cluster span { + line-height: 30px; + } \ No newline at end of file diff --git a/pybossa/static/css/stats/MarkerCluster.css b/pybossa/static/css/stats/MarkerCluster.css new file mode 100644 index 0000000000..a915c1a4bf --- /dev/null +++ b/pybossa/static/css/stats/MarkerCluster.css @@ -0,0 +1,6 @@ +.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { + -webkit-transition: -webkit-transform 0.25s ease-out, opacity 0.25s ease-in; + -moz-transition: -moz-transform 0.25s ease-out, opacity 0.25s ease-in; + -o-transition: -o-transform 0.25s ease-out, opacity 0.25s ease-in; + transition: transform 0.25s ease-out, opacity 0.25s ease-in; + } diff --git a/pybossa/static/css/stats/stats.css b/pybossa/static/css/stats/stats.css new file mode 100644 index 0000000000..aee7aed958 --- /dev/null +++ b/pybossa/static/css/stats/stats.css @@ -0,0 +1,99 @@ +html, +body { + background-color:#efeeee; +} + +h1 { + font-weight:normal; + margin:0px; +} +h1 strong { + color:#2681c0; +} +.alert-info { + background-color: #2681c0; + border:none; + color: #FFF; + text-shadow:none; +} +.alert-info .close { + color: #FFFFFF; + opacity: 1.0; + text-shadow: none; +} + +#heading { + margin-top:30px; + margin-bottom:20px; +} +#heading .span1 img { + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +[id*="card"] .well { + background-color:#FFF; +} +[id*="card"] .well h2 { + background-color:#d4d3d3; + margin: -19px -19px 30px; + padding:10px 20px; + font-size:14px; + line-height:normal; + -webkit-border-top-left-radius: 4px; + -webkit-border-top-right-radius: 4px; + -moz-border-radius-topleft: 4px; + -moz-border-radius-topright: 4px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} +[id*="card"] .well h2 small { + float: right; + font-size: 14px; + color: #6C6B6B; + background-image: url(/static/img/info-icon.png); + background-repeat: no-repeat; + background-position: left center; + padding-left:25px; + height:20px; + line-height:20px; +} + +.table-hover tbody tr:hover td, +.table-hover tbody tr:hover th { + background-color: rgb(200,200,200); +} + +a.pybossa { + display:inline-block; + position:absolute; + top:30px; + right:30px; + opacity:0.1; +} +a.pybossa:hover { + opacity:0.3; +} + +/* Desktop */ +@media (min-width: 768px) { + h1 { + font-size:24px; + } + #heading [class*="span"]:nth-of-type(2) { + margin-left:0px; + } + #heading .span1 { + width:54px; + } + #heading .span1 img { + max-width:44px; + } +} +/* Landscape phones and down */ +@media (max-width: 480px) { + a.pybossa { + position:relative; + margin:30px; + } +} diff --git a/pybossa/static/img/info-icon.png b/pybossa/static/img/info-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e1a40c9e8ba1a84061cd36a612d1bcc481336d01 GIT binary patch literal 1229 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+m{l@EB1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvO-#2=9ZF3nBND}m`vLFl!>sTY(OatnYqyQCInmZhe+73JqDfIV%MiQ6rvIL(9V zO~LIJGn{($fsWA!MJ-ZP!-Rn82gHOYTp$OY^i%VI>AeV;uyb?Bxct5&oz&|zW4i;CPWI>4{1TPP^j}^fTzvBXj$_(#bN=X#79IXP2^s;D%{^`O zl$U#N>Cx-dpZBq8=7|RN#nR8Zwg1lP<5_XC!TAqMp1ruFf^Deeg(Hp27c|<{u>Ny< znVH5P_-)TKkrTT=^s2_DTfe$>Ir6EP*oN+H&$KEITdJ8}zcyKX>+|#7%AK3^0$jhx ozk48CK5O;`(SIjq)v-!2Je}Z}R`p`mV^9I(>FVdQ&MBb@01jNPm;e9( literal 0 HcmV?d00001 diff --git a/pybossa/static/js/stats/flotr2.min.js b/pybossa/static/js/stats/flotr2.min.js new file mode 100644 index 0000000000..afdc7ad92a --- /dev/null +++ b/pybossa/static/js/stats/flotr2.min.js @@ -0,0 +1,27 @@ +/*! + * bean.js - copyright Jacob Thornton 2011 + * https://github.com/fat/bean + * MIT License + * special thanks to: + * dean edwards: http://dean.edwards.name/ + * dperini: https://github.com/dperini/nwevents + * the entire mootools team: github.com/mootools/mootools-core + *//*global module:true, define:true*/ +!function(a,b,c){typeof module!="undefined"?module.exports=c(a,b):typeof define=="function"&&typeof define.amd=="object"?define(c):b[a]=c(a,b)}("bean",this,function(a,b){var c=window,d=b[a],e=/over|out/,f=/[^\.]*(?=\..*)\.|.*/,g=/\..*/,h="addEventListener",i="attachEvent",j="removeEventListener",k="detachEvent",l=document||{},m=l.documentElement||{},n=m[h],o=n?h:i,p=Array.prototype.slice,q=/click|mouse|menu|drag|drop/i,r=/^touch|^gesture/i,s={one:1},t=function(a,b,c){for(c=0;c0){b=b.split(" ");for(j=b.length;j--;)G(a,b[j],c);return a}h=l&&b.replace(g,""),h&&u[h]&&(h=u[h].type);if(!b||l){if(i=l&&b.replace(f,""))i=i.split(".");k(a,h,c,i)}else if(typeof b=="function")k(a,null,b);else for(d in b)b.hasOwnProperty(d)&&G(a,d,b[d]);return a},H=function(a,b,c,d,e){var f,g,h,i,j=c,k=c&&typeof c=="string";if(b&&!c&&typeof b=="object")for(f in b)b.hasOwnProperty(f)&&H.apply(this,[a,f,b[f]]);else{i=arguments.length>3?p.call(arguments,3):[],g=(k?c:b).split(" "),k&&(c=F(b,j=d,e))&&(i=p.call(i,1)),this===s&&(c=C(G,a,b,c,j));for(h=g.length;h--;)E(a,g[h],c,j,i)}return a},I=function(){return H.apply(s,arguments)},J=n?function(a,b,d){var e=l.createEvent(a?"HTMLEvents":"UIEvents");e[a?"initEvent":"initUIEvent"](b,!0,!0,c,1),d.dispatchEvent(e)}:function(a,b,c){c=w(c,a),a?c.fireEvent("on"+b,l.createEventObject()):c["_on"+b]++},K=function(a,b,c){var d,e,h,i,j,k=b.split(" ");for(d=k.length;d--;){b=k[d].replace(g,"");if(i=k[d].replace(f,""))i=i.split(".");if(!i&&!c&&a[o])J(t[b],b,a);else{j=y.get(a,b),c=[!1].concat(c);for(e=0,h=j.length;e=d.computed&&(d={value:a,computed:g})}),d.value},w.min=function(a,b,c){if(!b&&w.isArray(a))return Math.min.apply(Math,a);var d={computed:Infinity};return x(a,function(a,e,f){var g=b?b.call(c,a,e,f):a;gd?1:0}),"value")},w.groupBy=function(a,b){var c={};return x(a,function(a,d){var e=b(a,d);(c[e]||(c[e]=[])).push(a)}),c},w.sortedIndex=function(a,b,c){c||(c=w.identity);var d=0,e=a.length;while(d>1;c(a[f])=0})})},w.difference=function(a,b){return w.filter(a,function(a){return!w.include(b,a)})},w.zip=function(){var a=g.call(arguments),b=w.max(w.pluck(a,"length")),c=new Array(b);for(var d=0;d=0;c--)b=[a[c].apply(this,b)];return b[0]}},w.after=function(a,b){return function(){if(--a<1)return b.apply(this,arguments)}},w.keys=u||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var b=[];for(var c in a)j.call(a,c)&&(b[b.length]=c);return b},w.values=function(a){return w.map(a,w.identity)},w.functions=w.methods=function(a){var b=[];for(var c in a)w.isFunction(a[c])&&b.push(c);return b.sort()},w.extend=function(a){return x(g.call(arguments,1),function(b){for(var c in b)b[c]!==void 0&&(a[c]=b[c])}),a},w.defaults=function(a){return x(g.call(arguments,1),function(b){for(var c in b)a[c]==null&&(a[c]=b[c])}),a},w.clone=function(a){return w.isArray(a)?a.slice():w.extend({},a)},w.tap=function(a,b){return b(a),a},w.isEqual=function(a,b){if(a===b)return!0;var c=typeof a,d=typeof b;if(c!=d)return!1;if(a==b)return!0;if(!a&&b||a&&!b)return!1;a._chain&&(a=a._wrapped),b._chain&&(b=b._wrapped);if(a.isEqual)return a.isEqual(b);if(b.isEqual)return b.isEqual(a);if(w.isDate(a)&&w.isDate(b))return a.getTime()===b.getTime();if(w.isNaN(a)&&w.isNaN(b))return!1;if(w.isRegExp(a)&&w.isRegExp(b))return a.source===b.source&&a.global===b.global&&a.ignoreCase===b.ignoreCase&&a.multiline===b.multiline;if(c!=="object")return!1;if(a.length&&a.length!==b.length)return!1;var e=w.keys(a),f=w.keys(b);if(e.length!=f.length)return!1;for(var g in a)if(!(g in b)||!w.isEqual(a[g],b[g]))return!1;return!0},w.isEmpty=function(a){if(w.isArray(a)||w.isString(a))return a.length===0;for(var b in a)if(j.call(a,b))return!1;return!0},w.isElement=function(a){return!!a&&a.nodeType==1},w.isArray=t||function(a){return i.call(a)==="[object Array]"},w.isObject=function(a){return a===Object(a)},w.isArguments=function(a){return!!a&&!!j.call(a,"callee")},w.isFunction=function(a){return!!(a&&a.constructor&&a.call&&a.apply)},w.isString=function(a){return!!(a===""||a&&a.charCodeAt&&a.substr)},w.isNumber=function(a){return!!(a===0||a&&a.toExponential&&a.toFixed)},w.isNaN=function(a){return a!==a},w.isBoolean=function(a){return a===!0||a===!1},w.isDate=function(a){return!!(a&&a.getTimezoneOffset&&a.setUTCFullYear)},w.isRegExp=function(a){return!(!(a&&a.test&&a.exec)||!a.ignoreCase&&a.ignoreCase!==!1)},w.isNull=function(a){return a===null},w.isUndefined=function(a){return a===void 0},w.noConflict=function(){return a._=b,this},w.identity=function(a){return a},w.times=function(a,b,c){for(var d=0;d/g,interpolate:/<%=([\s\S]+?)%>/g},w.template=function(a,b){var c=w.templateSettings,d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(c.interpolate,function(a,b){return"',"+b.replace(/\\'/g,"'")+",'"}).replace(c.evaluate||null,function(a,b){return"');"+b.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+"__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",e=new Function("obj",d);return b?e(b):e};var B=function(a){this._wrapped=a};w.prototype=B.prototype;var C=function(a,b){return b?w(a).chain():a},D=function(a,b){B.prototype[a]=function(){var a=g.call(arguments);return h.call(a,this._wrapped),C(b.apply(w,a),this._chain)}};w.mixin(w),x(["pop","push","reverse","shift","sort","splice","unshift"],function(a){var b=d[a];B.prototype[a]=function(){return b.apply(this._wrapped,arguments),C(this._wrapped,this._chain)}}),x(["concat","join","slice"],function(a){var b=d[a];B.prototype[a]=function(){return C(b.apply(this._wrapped,arguments),this._chain)}}),B.prototype.chain=function(){return this._chain=!0,this},B.prototype.value=function(){return this._wrapped}})(); +/** + * Flotr2 (c) 2012 Carl Sutherland + * MIT License + * Special thanks to: + * Flotr: http://code.google.com/p/flotr/ (fork) + * Flot: https://github.com/flot/flot (original fork) + */ +(function(){var a=this,b=this.Flotr,c;c={_:_,bean:bean,isIphone:/iphone/i.test(navigator.userAgent),isIE:navigator.appVersion.indexOf("MSIE")!=-1?parseFloat(navigator.appVersion.split("MSIE")[1]):!1,graphTypes:{},plugins:{},addType:function(a,b){c.graphTypes[a]=b,c.defaultOptions[a]=b.options||{},c.defaultOptions.defaultType=c.defaultOptions.defaultType||a},addPlugin:function(a,b){c.plugins[a]=b,c.defaultOptions[a]=b.options||{}},draw:function(a,b,d,e){return e=e||c.Graph,new e(a,b,d)},merge:function(a,b){var d,e,f=b||{};for(d in a)e=a[d],e&&typeof e=="object"?e.constructor===Array?f[d]=this._.clone(e):e.constructor!==RegExp&&!this._.isElement(e)&&!e.jquery?f[d]=c.merge(e,b?b[d]:undefined):f[d]=e:f[d]=e;return f},clone:function(a){return c.merge(a,{})},getTickSize:function(a,b,d,e){var f=(d-b)/a,g=c.getMagnitude(f),h=10,i=f/g;return i<1.5?h=1:i<2.25?h=2:i<3?h=e===0?2:2.5:i<7.5&&(h=5),h*g},defaultTickFormatter:function(a,b){return a+""},defaultTrackFormatter:function(a){return"("+a.x+", "+a.y+")"},engineeringNotation:function(a,b,c){var d=["Y","Z","E","P","T","G","M","k",""],e=["y","z","a","f","p","n","ยต","m",""],f=d.length;c=c||1e3,b=Math.pow(10,b||2);if(a===0)return 0;if(a>1)while(f--&&a>=c)a/=c;else{d=e,f=d.length;while(f--&&a<1)a*=c}return Math.round(a*b)/b+d[f]},getMagnitude:function(a){return Math.pow(10,Math.floor(Math.log(a)/Math.LN10))},toPixel:function(a){return Math.floor(a)+.5},toRad:function(a){return-a*(Math.PI/180)},floorInBase:function(a,b){return b*Math.floor(a/b)},drawText:function(a,b,d,e,f){if(!a.fillText){a.drawText(b,d,e,f);return}f=this._.extend({size:c.defaultOptions.fontSize,color:"#000000",textAlign:"left",textBaseline:"bottom",weight:1,angle:0},f),a.save(),a.translate(d,e),a.rotate(f.angle),a.fillStyle=f.color,a.font=(f.weight>1?"bold ":"")+f.size*1.3+"px sans-serif",a.textAlign=f.textAlign,a.textBaseline=f.textBaseline,a.fillText(b,0,0),a.restore()},getBestTextAlign:function(a,b){return b=b||{textAlign:"center",textBaseline:"middle"},a+=c.getTextAngleFromAlign(b),Math.abs(Math.cos(a))>.01&&(b.textAlign=Math.cos(a)>0?"right":"left"),Math.abs(Math.sin(a))>.01&&(b.textBaseline=Math.sin(a)>0?"top":"bottom"),b},alignTable:{"right middle":0,"right top":Math.PI/4,"center top":Math.PI/2,"left top":3*(Math.PI/4),"left middle":Math.PI,"left bottom":-3*(Math.PI/4),"center bottom":-Math.PI/2,"right bottom":-Math.PI/4,"center middle":0},getTextAngleFromAlign:function(a){return c.alignTable[a.textAlign+" "+a.textBaseline]||0},noConflict:function(){return a.Flotr=b,this}},a.Flotr=c})(),Flotr.defaultOptions={colors:["#00A8F0","#C0D800","#CB4B4B","#4DA74D","#9440ED"],ieBackgroundColor:"#FFFFFF",title:null,subtitle:null,shadowSize:4,defaultType:null,HtmlText:!0,fontColor:"#545454",fontSize:7.5,resolution:1,parseFloat:!0,preventDefault:!0,xaxis:{ticks:null,minorTicks:null,showLabels:!0,showMinorLabels:!1,labelsAngle:0,title:null,titleAngle:0,noTicks:5,minorTickFreq:null,tickFormatter:Flotr.defaultTickFormatter,tickDecimals:null,min:null,max:null,autoscale:!1,autoscaleMargin:0,color:null,mode:"normal",timeFormat:null,timeMode:"UTC",timeUnit:"millisecond",scaling:"linear",base:Math.E,titleAlign:"center",margin:!0},x2axis:{},yaxis:{ticks:null,minorTicks:null,showLabels:!0,showMinorLabels:!1,labelsAngle:0,title:null,titleAngle:90,noTicks:5,minorTickFreq:null,tickFormatter:Flotr.defaultTickFormatter,tickDecimals:null,min:null,max:null,autoscale:!1,autoscaleMargin:0,color:null,scaling:"linear",base:Math.E,titleAlign:"center",margin:!0},y2axis:{titleAngle:270},grid:{color:"#545454",backgroundColor:null,backgroundImage:null,watermarkAlpha:.4,tickColor:"#DDDDDD",labelMargin:3,verticalLines:!0,minorVerticalLines:null,horizontalLines:!0,minorHorizontalLines:null,outlineWidth:1,outline:"nsew",circular:!1},mouse:{track:!1,trackAll:!1,position:"se",relative:!1,trackFormatter:Flotr.defaultTrackFormatter,margin:5,lineColor:"#FF3F19",trackDecimals:1,sensibility:2,trackY:!0,radius:3,fillColor:null,fillOpacity:.4}},function(){function b(a,b,c,d){this.rgba=["r","g","b","a"];var e=4;while(-1<--e)this[this.rgba[e]]=arguments[e]||(e==3?1:0);this.normalize()}var a=Flotr._,c={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]};b.prototype={scale:function(b,c,d,e){var f=4;while(-1<--f)a.isUndefined(arguments[f])||(this[this.rgba[f]]*=arguments[f]);return this.normalize()},alpha:function(b){return!a.isUndefined(b)&&!a.isNull(b)&&(this.a=b),this.normalize()},clone:function(){return new b(this.r,this.b,this.g,this.a)},limit:function(a,b,c){return Math.max(Math.min(a,c),b)},normalize:function(){var a=this.limit;return this.r=a(parseInt(this.r,10),0,255),this.g=a(parseInt(this.g,10),0,255),this.b=a(parseInt(this.b,10),0,255),this.a=a(this.a,0,1),this},distance:function(a){if(!a)return;a=new b.parse(a);var c=0,d=3;while(-1<--d)c+=Math.abs(this[this.rgba[d]]-a[this.rgba[d]]);return c},toString:function(){return this.a>=1?"rgb("+[this.r,this.g,this.b].join(",")+")":"rgba("+[this.r,this.g,this.b,this.a].join(",")+")"},contrast:function(){var a=1-(.299*this.r+.587*this.g+.114*this.b)/255;return a<.5?"#000000":"#ffffff"}},a.extend(b,{parse:function(a){if(a instanceof b)return a;var d;if(d=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(a))return new b(parseInt(d[1],16),parseInt(d[2],16),parseInt(d[3],16));if(d=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(a))return new b(parseInt(d[1],10),parseInt(d[2],10),parseInt(d[3],10));if(d=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(a))return new b(parseInt(d[1]+d[1],16),parseInt(d[2]+d[2],16),parseInt(d[3]+d[3],16));if(d=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(a))return new b(parseInt(d[1],10),parseInt(d[2],10),parseInt(d[3],10),parseFloat(d[4]));if(d=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(a))return new b(parseFloat(d[1])*2.55,parseFloat(d[2])*2.55,parseFloat(d[3])*2.55);if(d=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(a))return new b(parseFloat(d[1])*2.55,parseFloat(d[2])*2.55,parseFloat(d[3])*2.55,parseFloat(d[4]));var e=(a+"").replace(/^\s*([\S\s]*?)\s*$/,"$1").toLowerCase();return e=="transparent"?new b(255,255,255,0):(d=c[e])?new b(d[0],d[1],d[2]):new b(0,0,0,0)},processColor:function(c,d){var e=d.opacity;if(!c)return"rgba(0, 0, 0, 0)";if(c instanceof b)return c.alpha(e).toString();if(a.isString(c))return b.parse(c).alpha(e).toString();var f=c.colors?c:{colors:c};if(!d.ctx)return a.isArray(f.colors)?b.parse(a.isArray(f.colors[0])?f.colors[0][1]:f.colors[0]).alpha(e).toString():"rgba(0, 0, 0, 0)";f=a.extend({start:"top",end:"bottom"},f),/top/i.test(f.start)&&(d.x1=0),/left/i.test(f.start)&&(d.y1=0),/bottom/i.test(f.end)&&(d.x2=0),/right/i.test(f.end)&&(d.y2=0);var g,h,i,j=d.ctx.createLinearGradient(d.x1,d.y1,d.x2,d.y2);for(g=0;g=m)break}m=e[p][0],n=e[p][1],n=="year"&&(m=Flotr.getTickSize(f.noTicks*d.year,i,j,0),m==.5&&(n="month",m=6)),a.tickUnit=n,a.tickSize=m;var r=m*d[n];q=new Date(i);switch(n){case"millisecond":s("Milliseconds");break;case"second":s("Seconds");break;case"minute":s("Minutes");break;case"hour":s("Hours");break;case"month":s("Month");break;case"year":s("FullYear")}r>=d.second&&b(q,"Milliseconds",g,0),r>=d.minute&&b(q,"Seconds",g,0),r>=d.hour&&b(q,"Minutes",g,0),r>=d.day&&b(q,"Hours",g,0),r>=d.day*4&&b(q,"Date",g,1),r>=d.year&&b(q,"Month",g,0);var t=0,u=NaN,v;do{v=u,u=q.getTime(),l.push({v:u/h,label:o(u/h,a)});if(n=="month")if(m<1){b(q,"Date",g,1);var w=q.getTime();b(q,"Month",g,c(q,"Month",g)+1);var x=q.getTime();q.setTime(u+t*d.hour+(x-w)*m),t=c(q,"Hours",g),b(q,"Hours",g,0)}else b(q,"Month",g,c(q,"Month",g)+m);else n=="year"?b(q,"FullYear",g,c(q,"FullYear",g)+m):q.setTime(u+r)}while(u0)return{x:b.touches[0].pageX,y:b.touches[0].pageY};if(!a._.isUndefined(b.changedTouches)&&b.changedTouches.length>0)return{x:b.changedTouches[0].pageX,y:b.changedTouches[0].pageY};if(b.pageX||b.pageY)return{x:b.pageX,y:b.pageY};if(b.clientX||b.clientY){var c=document,d=c.body,e=c.documentElement;return{x:b.clientX+d.scrollLeft+e.scrollLeft,y:b.clientY+d.scrollTop+e.scrollTop}}}}}(),function(){var a=Flotr,b=a.DOM,c=a._,d=function(a){this.o=a};d.prototype={dimensions:function(a,b,c,d){return a?this.o.html?this.html(a,this.o.element,c,d):this.canvas(a,b):{width:0,height:0}},canvas:function(b,c){if(!this.o.textEnabled)return;c=c||{};var d=this.measureText(b,c),e=d.width,f=c.size||a.defaultOptions.fontSize,g=c.angle||0,h=Math.cos(g),i=Math.sin(g),j=2,k=6,l;return l={width:Math.abs(h*e)+Math.abs(i*f)+j,height:Math.abs(i*e)+Math.abs(h*f)+k},l},html:function(a,c,d,e){var f=b.create("div");return b.setStyles(f,{position:"absolute",top:"-10000px"}),b.insert(f,'
'+a+"
"),b.insert(this.o.element,f),b.size(f)},measureText:function(b,d){var e=this.o.ctx,f;return!e.fillText||a.isIphone&&e.measure?{width:e.measure(b,d)}:(d=c.extend({size:a.defaultOptions.fontSize,weight:1,angle:0},d),e.save(),e.font=(d.weight>1?"bold ":"")+d.size*1.3+"px sans-serif",f=e.measureText(b),e.restore(),f)}},Flotr.Text=d}(),function(){function e(a,c,d){return b.observe.apply(this,arguments),this._handles.push(arguments),this}var a=Flotr.DOM,b=Flotr.EventAdapter,c=Flotr._,d=Flotr;Graph=function(a,e,f){this._setEl(a),this._initMembers(),this._initPlugins(),b.fire(this.el,"flotr:beforeinit",[this]),this.data=e,this.series=d.Series.getSeries(e),this._initOptions(f),this._initGraphTypes(),this._initCanvas(),this._text=new d.Text({element:this.el,ctx:this.ctx,html:this.options.HtmlText,textEnabled:this.textEnabled}),b.fire(this.el,"flotr:afterconstruct",[this]),this._initEvents(),this.findDataRanges(),this.calculateSpacing(),this.draw(c.bind(function(){b.fire(this.el,"flotr:afterinit",[this])},this))},Graph.prototype={destroy:function(){b.fire(this.el,"flotr:destroy"),c.each(this._handles,function(a){b.stopObserving.apply(this,a)}),this._handles=[],this.el.graph=null},observe:e,_observe:e,processColor:function(a,b){var e={x1:0,y1:0,x2:this.plotWidth,y2:this.plotHeight,opacity:1,ctx:this.ctx};return c.extend(e,b),d.Color.processColor(a,e)},findDataRanges:function(){var a=this.axes,b,e,f;c.each(this.series,function(a){f=a.getRange(),f&&(b=a.xaxis,e=a.yaxis,b.datamin=Math.min(f.xmin,b.datamin),b.datamax=Math.max(f.xmax,b.datamax),e.datamin=Math.min(f.ymin,e.datamin),e.datamax=Math.max(f.ymax,e.datamax),b.used=b.used||f.xused,e.used=e.used||f.yused)},this),!a.x.used&&!a.x2.used&&(a.x.used=!0),!a.y.used&&!a.y2.used&&(a.y.used=!0),c.each(a,function(a){a.calculateRange()});var g=c.keys(d.graphTypes),h=!1;c.each(this.series,function(a){if(a.hide)return;c.each(g,function(b){a[b]&&a[b].show&&(this.extendRange(b,a),h=!0)},this),h||this.extendRange(this.options.defaultType,a)},this)},extendRange:function(a,b){this[a].extendRange&&this[a].extendRange(b,b.data,b[a],this[a]),this[a].extendYRange&&this[a].extendYRange(b.yaxis,b.data,b[a],this[a]),this[a].extendXRange&&this[a].extendXRange(b.xaxis,b.data,b[a],this[a])},calculateSpacing:function(){var a=this.axes,b=this.options,d=this.series,e=b.grid.labelMargin,f=this._text,g=a.x,h=a.x2,i=a.y,j=a.y2,k=b.grid.outlineWidth,l,m,n,o;c.each(a,function(a){a.calculateTicks(),a.calculateTextDimensions(f,b)}),o=f.dimensions(b.title,{size:b.fontSize*1.5},"font-size:1em;font-weight:bold;","flotr-title"),this.titleHeight=o.height,o=f.dimensions(b.subtitle,{size:b.fontSize},"font-size:smaller;","flotr-subtitle"),this.subtitleHeight=o.height;for(m=0;m1&&(this.multitouches=c.touches),b.fire(a,"flotr:mousedown",[event,this]),this.observe(document,"touchend",d)},this)),this.observe(this.overlay,"touchmove",c.bind(function(c){var d=this.getEventPosition(c);this.options.preventDefault&&c.preventDefault(),e=!0,this.multitouches||c.touches&&c.touches.length>1?this.multitouches=c.touches:f||b.fire(a,"flotr:mousemove",[event,d,this]),this.lastMousePos=d},this))):this.observe(this.overlay,"mousedown",c.bind(this.mouseDownHandler,this)).observe(a,"mousemove",c.bind(this.mouseMoveHandler,this)).observe(this.overlay,"click",c.bind(this.clickHandler,this)).observe(a,"mouseout",function(){b.fire(a,"flotr:mouseout")})},_initCanvas:function(){function k(e,f){return e||(e=a.create("canvas"),typeof FlashCanvas!="undefined"&&typeof e.getContext=="function"&&FlashCanvas.initElement(e),e.className="flotr-"+f,e.style.cssText="position:absolute;left:0px;top:0px;",a.insert(b,e)),c.each(i,function(b,c){a.show(e);if(f=="canvas"&&e.getAttribute(c)===b)return;e.setAttribute(c,b*d.resolution),e.style[c]=b+"px"}),e.context_=null,e}function l(a){window.G_vmlCanvasManager&&window.G_vmlCanvasManager.initElement(a);var b=a.getContext("2d");return window.G_vmlCanvasManager||b.scale(d.resolution,d.resolution),b}var b=this.el,d=this.options,e=b.children,f=[],g,h,i,j;for(h=e.length;h--;)g=e[h],!this.canvas&&g.className==="flotr-canvas"?this.canvas=g:!this.overlay&&g.className==="flotr-overlay"?this.overlay=g:f.push(g);for(h=f.length;h--;)b.removeChild(f[h]);a.setStyles(b,{position:"relative"}),i={},i.width=b.clientWidth,i.height=b.clientHeight;if(i.width<=0||i.height<=0||d.resolution<=0)throw"Invalid dimensions for plot, width = "+i.width+", height = "+i.height+", resolution = "+d.resolution;this.canvas=k(this.canvas,"canvas"),this.overlay=k(this.overlay,"overlay"),this.ctx=l(this.canvas),this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.octx=l(this.overlay),this.octx.clearRect(0,0,this.overlay.width,this.overlay.height),this.canvasHeight=i.height,this.canvasWidth=i.width,this.textEnabled=!!this.ctx.drawText||!!this.ctx.fillText},_initPlugins:function(){c.each(d.plugins,function(a,b){c.each(a.callbacks,function(a,b){this.observe(this.el,b,c.bind(a,this))},this),this[b]=d.clone(a),c.each(this[b],function(a,d){c.isFunction(a)&&(this[b][d]=c.bind(a,this))},this)},this)},_initOptions:function(a){var e=d.clone(d.defaultOptions);e.x2axis=c.extend(c.clone(e.xaxis),e.x2axis),e.y2axis=c.extend(c.clone(e.yaxis),e.y2axis),this.options=d.merge(a||{},e),this.options.grid.minorVerticalLines===null&&this.options.xaxis.scaling==="logarithmic"&&(this.options.grid.minorVerticalLines=!0),this.options.grid.minorHorizontalLines===null&&this.options.yaxis.scaling==="logarithmic"&&(this.options.grid.minorHorizontalLines=!0),b.fire(this.el,"flotr:afterinitoptions",[this]),this.axes=d.Axis.getAxes(this.options);var f=[],g=[],h=this.series.length,i=this.series.length,j=this.options.colors,k=[],l=0,m,n,o,p;for(n=i-1;n>-1;--n)m=this.series[n].color,m&&(--i,c.isNumber(m)?f.push(m):k.push(d.Color.parse(m)));for(n=f.length-1;n>-1;--n)i=Math.max(i,f[n]+1);for(n=0;g.length=j.length&&(n=0,++l)}for(n=0,o=0;n10?b.minorTickFreq=0:g-h>5?b.minorTickFreq=2:b.minorTickFreq=5)}else a.tickSize=Flotr.getTickSize(b.noTicks,c,d,b.tickDecimals);a.min=c,a.max=d,b.min===null&&b.autoscale&&(a.min-=a.tickSize*e,a.min<0&&a.datamin>=0&&(a.min=0),a.min=a.tickSize*Math.floor(a.min/a.tickSize)),b.max===null&&b.autoscale&&(a.max+=a.tickSize*e,a.max>0&&a.datamax<=0&&a.datamax!=a.datamin&&(a.max=0),a.max=a.tickSize*Math.ceil(a.max/a.tickSize)),a.min==a.max&&(a.max=a.min+1)},calculateTextDimensions:function(a,b){var c="",d,e;if(this.options.showLabels)for(e=0;ec.length&&(c=this.ticks[e].label);this.maxLabel=a.dimensions(c,{size:b.fontSize,angle:Flotr.toRad(this.options.labelsAngle)},"font-size:smaller;","flotr-grid-label"),this.titleSize=a.dimensions(this.options.title,{size:b.fontSize*1.2,angle:Flotr.toRad(this.options.titleAngle)},"font-weight:bold;","flotr-axis-title")},_cleanUserTicks:function(b,c){var d=this,e=this.options,f,g,h,i;a.isFunction(b)&&(b=b({min:d.min,max:d.max}));for(g=0;g1?i[1]:e.tickFormatter(f,{min:d.min,max:d.max})):(f=i,h=e.tickFormatter(f,{min:this.min,max:this.max})),c[g]={v:f,label:h}},_calculateTimeTicks:function(){this.ticks=Flotr.Date.generator(this)},_calculateLogTicks:function(){var a=this,b=a.options,c,d,e=Math.log(a.max);b.base!=Math.E&&(e/=Math.log(b.base)),e=Math.ceil(e);var f=Math.log(a.min);b.base!=Math.E&&(f/=Math.log(b.base)),f=Math.ceil(f);for(i=f;ie&&(e=i,g=!0)),j!==null&&(jf&&(f=j,h=!0));return{xmin:c,xmax:e,ymin:d,ymax:f,xused:g,yused:h}}},a.extend(b,{getSeries:function(c){return a.map(c,function(c){var d;return c.data?(d=new b,a.extend(d,c)):d=new b({data:c}),d})}}),Flotr.Series=b}(),Flotr.addType("lines",{options:{show:!1,lineWidth:2,fill:!1,fillBorder:!1,fillColor:null,fillOpacity:.4,steps:!1,stacked:!1},stack:{values:[]},draw:function(a){var b=a.context,c=a.lineWidth,d=a.shadowSize,e;b.save(),b.lineJoin="round",d&&(b.lineWidth=d/2,e=c/2+b.lineWidth/2,b.strokeStyle="rgba(0,0,0,0.1)",this.plot(a,e+d/2,!1),b.strokeStyle="rgba(0,0,0,0.2)",this.plot(a,e,!1)),b.lineWidth=c,b.strokeStyle=a.color,this.plot(a,0,!0),b.restore()},plot:function(a,b,c){function w(){!b&&a.fill&&o&&(p=g(o[0]),d.fillStyle=a.fillStyle,d.lineTo(q,n),d.lineTo(p,n),d.lineTo(p,h(o[1])),d.fill(),a.fillBorder&&d.stroke())}var d=a.context,e=a.width,f=a.height,g=a.xScale,h=a.yScale,i=a.data,j=a.stacked?this.stack:!1,k=i.length-1,l=null,m=null,n=h(0),o=null,p,q,r,s,t,u,v;if(k<1)return;d.beginPath();for(v=0;v0&&i[v][1]&&(d.stroke(),w(),o=null,d.closePath(),d.beginPath());continue}p=g(i[v][0]),q=g(i[v+1][0]),o===null&&(o=i[v]),j?(t=j.values[i[v][0]]||0,u=j.values[i[v+1][0]]||j.values[i[v][0]]||0,r=h(i[v][1]+t),s=h(i[v+1][1]+u),c&&(j.values[i[v][0]]=i[v][1]+t,v==k-1&&(j.values[i[v+1][0]]=i[v+1][1]+u))):(r=h(i[v][1]),s=h(i[v+1][1]));if(r>f&&s>f||r<0&&s<0||p<0&&q<0||p>e&&q>e)continue;(l!=p||m!=r+b)&&d.moveTo(p,r+b),l=q,m=s+b,a.steps?(d.lineTo(l+b/2,r+b),d.lineTo(l+b/2,m)):d.lineTo(l,m)}(!a.fill||a.fill&&!a.fillBorder)&&d.stroke(),w(),d.closePath()},extendYRange:function(a,b,c,d){var e=a.options;if(c.stacked&&(!e.max&&e.max!==0||!e.min&&e.min!==0)){var f=a.max,g=a.min,h=d.positiveSums||{},i=d.negativeSums||{},j,k;for(k=0;k0?(h[j]=(h[j]||0)+b[k][1],f=Math.max(f,h[j])):(i[j]=(i[j]||0)+b[k][1],g=Math.min(g,i[j]));d.negativeSums=i,d.positiveSums=h,a.max=f,a.min=g}c.steps&&(this.hit=function(a){var b=a.data,c=a.args,d=a.yScale,e=c[0],f=b.length,g=c[1],h=a.xInverse(e.relX),i=e.relY,j;for(j=0;j=b[j][0]&&h<=b[j+1][0]){Math.abs(d(b[j][1])-i)<8&&(g.x=b[j][0],g.y=b[j][1],g.index=j,g.seriesIndex=a.index);break}},this.drawHit=function(a){var b=a.context,c=a.args,d=a.data,e=a.xScale,f=c.index,g=e(c.x),h=a.yScale(c.y),i;d.length-1>f&&(i=a.xScale(d[f+1][0]),b.save(),b.strokeStyle=a.color,b.lineWidth=a.lineWidth,b.beginPath(),b.moveTo(g,h),b.lineTo(i,h),b.stroke(),b.closePath(),b.restore())},this.clearHit=function(a){var b=a.context,c=a.args,d=a.data,e=a.xScale,f=a.lineWidth,g=c.index,h=e(c.x),i=a.yScale(c.y),j;d.length-1>g&&(j=a.xScale(d[g+1][0]),b.clearRect(h-f,i-f,j-h+2*f,2*f))})}}),Flotr.addType("bars",{options:{show:!1,lineWidth:2,barWidth:1,fill:!0,fillColor:null,fillOpacity:.4,horizontal:!1,stacked:!1,centered:!0,topPadding:.1,grouped:!1},stack:{positive:[],negative:[],_positive:[],_negative:[]},draw:function(a){var b=a.context;this.current+=1,b.save(),b.lineJoin="miter",b.lineWidth=a.lineWidth,b.strokeStyle=a.color,a.fill&&(b.fillStyle=a.fillStyle),this.plot(a),b.restore()},plot:function(a){var b=a.data,c=a.context,d=a.shadowSize,e,f,g,h,i,j;if(b.length<1)return;this.translate(c,a.horizontal);for(e=0;e0?g.positive:g.negative,n=o[l]||n,o[l]=n+m),p=j(l-i),q=j(l+e-i),r=k(m+n),s=k(n),s<0&&(s=0),a===null||b===null?null:{x:l,y:m,xScale:j,yScale:k,top:r,left:Math.min(p,q)-h/2,width:Math.abs(q-p)-h,height:s-r}},hit:function(a){var b=a.data,c=a.args,d=c[0],e=c[1],f=a.xInverse(d.relX),g=a.yInverse(d.relY),h=this.getBarGeometry(f,g,a),i=h.width/2,j=h.left,k=h.y,l,m;for(m=b.length;m--;)l=this.getBarGeometry(b[m][0],b[m][1],a),(k>0&&kl.y)&&Math.abs(j-l.left)0?(j[l]=(j[l]||0)+m,g=Math.max(g,j[l])):(k[l]=(k[l]||0)+m,f=Math.min(f,k[l]));(i==1&&h||i==-1&&!h)&&c.topPadding&&(a.max===a.datamax||c.stacked&&this.stackMax!==g)&&(g+=c.topPadding*(g-f)),this.stackMin=f,this.stackMax=g,this.negativeSums=k,this.positiveSums=j,a.max=g,a.min=f}}),Flotr.addType("bubbles",{options:{show:!1,lineWidth:2,fill:!0,fillOpacity:.4,baseRadius:2},draw:function(a){var b=a.context,c=a.shadowSize;b.save(),b.lineWidth=a.lineWidth,b.fillStyle="rgba(0,0,0,0.05)",b.strokeStyle="rgba(0,0,0,0.05)",this.plot(a,c/2),b.strokeStyle="rgba(0,0,0,0.1)",this.plot(a,c/4),b.strokeStyle=a.color,b.fillStyle=a.fillStyle,this.plot(a),b.restore()},plot:function(a,b){var c=a.data,d=a.context,e,f,g,h,i;b=b||0;for(f=0;fr?"downFillColor":"upFillColor"],a.fill&&!a.barcharts&&(c.fillStyle="rgba(0,0,0,0.05)",c.fillRect(s+g,x+g,t-s,w-x),c.save(),c.globalAlpha=a.fillOpacity,c.fillStyle=k,c.fillRect(s,x+h,t-s,w-x),c.restore());if(h||i)m=Math.floor((s+t)/2)+j,c.strokeStyle=k,c.beginPath(),a.barcharts?(c.moveTo(m,Math.floor(v+f)),c.lineTo(m,Math.floor(u+f)),n=Math.floor(o+f)+.5,c.moveTo(Math.floor(s)+j,n),c.lineTo(m,n),n=Math.floor(r+f)+.5,c.moveTo(Math.floor(t)+j,n),c.lineTo(m,n)):(c.strokeRect(s,x+h,t-s,w-x),c.moveTo(m,Math.floor(x+h)),c.lineTo(m,Math.floor(v+h)),c.moveTo(m,Math.floor(w+h)),c.lineTo(m,Math.floor(u+h))),c.closePath(),c.stroke()}},extendXRange:function(a,b,c){a.options.max===null&&(a.max=Math.max(a.datamax+.5,a.max),a.min=Math.min(a.datamin-.5,a.min))}}),Flotr.addType("gantt",{options:{show:!1,lineWidth:2,barWidth:1,fill:!0,fillColor:null,fillOpacity:.4,centered:!0},draw:function(a){var b=this.ctx,c=a.gantt.barWidth,d=Math.min(a.gantt.lineWidth,c);b.save(),b.translate(this.plotOffset.left,this.plotOffset.top),b.lineJoin="miter",b.lineWidth=d,b.strokeStyle=a.color,b.save(),this.gantt.plotShadows(a,c,0,a.gantt.fill),b.restore();if(a.gantt.fill){var e=a.gantt.fillColor||a.color;b.fillStyle=this.processColor(e,{opacity:a.gantt.fillOpacity})}this.gantt.plot(a,c,0,a.gantt.fill),b.restore()},plot:function(a,b,c,d){var e=a.data;if(e.length<1)return;var f=a.xaxis,g=a.yaxis,h=this.ctx,i;for(i=0;if.max||sg.max)continue;pf.max&&(q=f.max,f.lastSerie!=a&&(n=!1)),rg.max&&(s=g.max,g.lastSerie!=a&&(n=!1)),d&&(h.beginPath(),h.moveTo(f.d2p(p),g.d2p(r)+c),h.lineTo(f.d2p(p),g.d2p(s)+c),h.lineTo(f.d2p(q),g.d2p(s)+c),h.lineTo(f.d2p(q),g.d2p(r)+c),h.fill(),h.closePath()),a.gantt.lineWidth&&(m||o||n)&&(h.beginPath(),h.moveTo(f.d2p(p),g.d2p(r)+c),h[m?"lineTo":"moveTo"](f.d2p(p),g.d2p(s)+c),h[n?"lineTo":"moveTo"](f.d2p(q),g.d2p(s)+c),h[o?"lineTo":"moveTo"](f.d2p(q),g.d2p(r)+c),h.stroke(),h.closePath())}},plotShadows:function(a,b,c){var d=a.data;if(d.length<1)return;var e,f,g,h,i=a.xaxis,j=a.yaxis,k=this.ctx,l=this.options.shadowSize;for(e=0;ei.max||pj.max)continue;mi.max&&(n=i.max),oj.max&&(p=j.max);var q=i.d2p(n)-i.d2p(m)-(i.d2p(n)+l<=this.plotWidth?0:l),r=j.d2p(o)-j.d2p(p)-(j.d2p(o)+l<=this.plotHeight?0:l);k.fillStyle="rgba(0,0,0,0.05)",k.fillRect(Math.min(i.d2p(m)+l,this.plotWidth),Math.min(j.d2p(p)+l,this.plotHeight),q,r)}},extendXRange:function(a){if(a.options.max===null){var b=a.min,c=a.max,d,e,f,g,h,i={},j={},k=null;for(d=0;db&&(b=a.max+g.barWidth)}}a.lastSerie=j,a.max=b,a.min=c,a.tickSize=Flotr.getTickSize(a.options.noTicks,c,b,a.options.tickDecimals)}}}),function(){function a(a){return typeof a=="object"&&a.constructor&&(Image?!0:a.constructor===Image)}Flotr.defaultMarkerFormatter=function(a){return Math.round(a.y*100)/100+""},Flotr.addType("markers",{options:{show:!1,lineWidth:1,color:"#000000",fill:!1,fillColor:"#FFFFFF",fillOpacity:.4,stroke:!1,position:"ct",verticalMargin:0,labelFormatter:Flotr.defaultMarkerFormatter,fontSize:Flotr.defaultOptions.fontSize,stacked:!1,stackingType:"b",horizontal:!1},stack:{positive:[],negative:[],values:[]},draw:function(a){function m(a,b){return g=d.negative[a]||0,f=d.positive[a]||0,b>0?(d.positive[a]=g+b,g+b):(d.negative[a]=f+b,f+b)}var b=a.data,c=a.context,d=a.stacked?a.stack:!1,e=a.stackingType,f,g,h,i,j,k,l;c.save(),c.lineJoin="round",c.lineWidth=a.lineWidth,c.strokeStyle="rgba(0,0,0,0.5)",c.fillStyle=a.fillStyle;for(i=0;i0?"top":"bottom",B,C,D;c.save(),c.translate(i/2,h/2),c.scale(1,q),C=Math.cos(u)*j,D=Math.sin(u)*j,f>0&&(this.plotSlice(C+f,D+f,n,s,t,c),l&&(c.fillStyle="rgba(0,0,0,0.1)",c.fill())),this.plotSlice(C,D,n,s,t,c),l&&(c.fillStyle=m,c.fill()),c.lineWidth=e,c.strokeStyle=k,c.stroke(),B={size:a.fontSize*1.2,color:a.fontColor,weight:1.5},v&&(a.htmlText||!a.textEnabled?(divStyle="position:absolute;"+A+":"+(h/2+(A==="top"?y:-y))+"px;",divStyle+=z+":"+(i/2+(z==="right"?-x:x))+"px;",p.push('
',v,"
")):(B.textAlign=z,B.textBaseline=A,Flotr.drawText(c,v,x,y,B)));if(a.htmlText||!a.textEnabled){var E=Flotr.DOM.node('
');Flotr.DOM.insert(E,p.join("")),Flotr.DOM.insert(a.element,E)}c.restore(),this.startAngle=t,this.slices=this.slices||[],this.slices.push({radius:Math.min(d.width,d.height)*g/2,x:C,y:D,explode:j,start:s,end:t})},plotSlice:function(a,b,c,d,e,f){f.beginPath(),f.moveTo(a,b),f.arc(a,b,c,d,e,!1),f.lineTo(a,b),f.closePath()},hit:function(a){var b=a.data[0],c=a.args,d=a.index,e=c[0],f=c[1],g=this.slices[d],h=e.relX-a.width/2,i=e.relY-a.height/2,j=Math.sqrt(h*h+i*i),k=Math.atan(i/h),l=Math.PI*2,m=g.explode||a.explode,n=g.start%l,o=g.end%l,p=a.epsilon;h<0?k+=Math.PI:h>0&&i<0&&(k+=l),jm&&(k>n&&ko&&(kn)||n===o&&(g.start===g.end&&Math.abs(k-n)p))&&(f.x=b[0],f.y=b[1],f.sAngle=n,f.eAngle=o,f.index=0,f.seriesIndex=d,f.fraction=b[1]/this.total)},drawHit:function(a){var b=a.context,c=this.slices[a.args.seriesIndex];b.save(),b.translate(a.width/2,a.height/2),this.plotSlice(c.x,c.y,c.radius,c.start,c.end,b),b.stroke(),b.restore()},clearHit:function(a){var b=a.context,c=this.slices[a.args.seriesIndex],d=2*a.lineWidth,e=c.radius+d;b.save(),b.translate(a.width/2,a.height/2),b.clearRect(c.x-e,c.y-e,2*e+d,2*e+d),b.restore()},extendYRange:function(a,b){this.total=(this.total||0)+b[0][1]}})}(),Flotr.addType("points",{options:{show:!1,radius:3,lineWidth:2,fill:!0,fillColor:"#FFFFFF",fillOpacity:1,hitRadius:null},draw:function(a){var b=a.context,c=a.lineWidth,d=a.shadowSize;b.save(),d>0&&(b.lineWidth=d/2,b.strokeStyle="rgba(0,0,0,0.1)",this.plot(a,d/2+b.lineWidth/2),b.strokeStyle="rgba(0,0,0,0.2)",this.plot(a,b.lineWidth/2)),b.lineWidth=a.lineWidth,b.strokeStyle=a.color,a.fill&&(b.fillStyle=a.fillStyle),this.plot(a),b.restore()},plot:function(a,b){var c=a.data,d=a.context,e=a.xScale,f=a.yScale,g,h,i;for(g=c.length-1;g>-1;--g){i=c[g][1];if(i===null)continue;h=e(c[g][0]),i=f(i);if(h<0||h>a.width||i<0||i>a.height)continue;d.beginPath(),b?d.arc(h,i+b,a.radius,0,Math.PI,!1):(d.arc(h,i,a.radius,0,2*Math.PI,!0),a.fill&&d.fill()),d.stroke(),d.closePath()}}}),Flotr.addType("radar",{options:{show:!1,lineWidth:2,fill:!0,fillOpacity:.4,radiusRatio:.9},draw:function(a){var b=a.context,c=a.shadowSize;b.save(),b.translate(a.width/2,a.height/2),b.lineWidth=a.lineWidth,b.fillStyle="rgba(0,0,0,0.05)",b.strokeStyle="rgba(0,0,0,0.05)",this.plot(a,c/2),b.strokeStyle="rgba(0,0,0,0.1)",this.plot(a,c/4),b.strokeStyle=a.color,b.fillStyle=a.fillStyle,this.plot(a),b.restore()},plot:function(a,b){var c=a.data,d=a.context,e=Math.min(a.height,a.width)*a.radiusRatio/2,f=2*Math.PI/c.length,g=-Math.PI/2,h,i;b=b||0,d.beginPath();for(h=0;hthis.plotWidth||b.relY>this.plotHeight){this.el.style.cursor=null,a.removeClass(this.el,"flotr-crosshair");return}d.hideCursor&&(this.el.style.cursor="none",a.addClass(this.el,"flotr-crosshair")),c.save(),c.strokeStyle=d.color,c.lineWidth=1,c.beginPath(),d.mode.indexOf("x")!=-1&&(c.moveTo(f,e.top),c.lineTo(f,e.top+this.plotHeight)),d.mode.indexOf("y")!=-1&&(c.moveTo(e.left,g),c.lineTo(e.left+this.plotWidth,g)),c.stroke(),c.restore()},clearCrosshair:function(){var a=this.plotOffset,b=this.lastMousePos,c=this.octx;b&&(c.clearRect(Math.round(b.relX)+a.left,a.top,1,this.plotHeight+1),c.clearRect(a.left,Math.round(b.relY)+a.top,this.plotWidth+1,1))}})}(),function(){function c(a,b,c,d){var e="image/"+a,f=b.toDataURL(e),g=new Image;return g.src=f,g}var a=Flotr.DOM,b=Flotr._;Flotr.addPlugin("download",{saveImage:function(d,e,f,g){var h=null;if(Flotr.isIE&&Flotr.isIE<9)return h=""+this.canvas.firstChild.innerHTML+"",window.open().document.write(h);if(d!=="jpeg"&&d!=="png")return;h=c(d,this.canvas,e,f);if(!b.isElement(h)||!g)return window.open(h.src);this.download.restoreCanvas(),a.hide(this.canvas),a.hide(this.overlay),a.setStyles({position:"absolute"}),a.insert(this.el,h),this.saveImageElement=h},restoreCanvas:function(){a.show(this.canvas),a.show(this.overlay),this.saveImageElement&&this.el.removeChild(this.saveImageElement),this.saveImageElement=null}})}(),function(){var a=Flotr.EventAdapter,b=Flotr._;Flotr.addPlugin("graphGrid",{callbacks:{"flotr:beforedraw":function(){this.graphGrid.drawGrid()},"flotr:afterdraw":function(){this.graphGrid.drawOutline()}},drawGrid:function(){function p(a){for(n=0;n=l.max||(a==l.min||a==l.max)&&e.outlineWidth)return;d(Math.floor(l.d2p(a))+c.lineWidth/2)})}function r(a){c.moveTo(a,0),c.lineTo(a,j)}function s(a){c.moveTo(0,a),c.lineTo(k,a)}var c=this.ctx,d=this.options,e=d.grid,f=e.verticalLines,g=e.horizontalLines,h=e.minorVerticalLines,i=e.minorHorizontalLines,j=this.plotHeight,k=this.plotWidth,l,m,n,o;(f||h||g||i)&&a.fire(this.el,"flotr:beforegrid",[this.axes.x,this.axes.y,d,this]),c.save(),c.lineWidth=1,c.strokeStyle=e.tickColor;if(e.circular){c.translate(this.plotOffset.left+k/2,this.plotOffset.top+j/2);var t=Math.min(j,k)*d.radar.radiusRatio/2,u=this.axes.x.ticks.length,v=2*(Math.PI/u),w=-Math.PI/2;c.beginPath(),l=this.axes.y,g&&p(l.ticks),i&&p(l.minorTicks),f&&b.times(u,function(a){c.moveTo(0,0),c.lineTo(Math.cos(a*v+w)*t,Math.sin(a*v+w)*t)}),c.stroke()}else c.translate(this.plotOffset.left,this.plotOffset.top),e.backgroundColor&&(c.fillStyle=this.processColor(e.backgroundColor,{x1:0,y1:0,x2:k,y2:j}),c.fillRect(0,0,k,j)),c.beginPath(),l=this.axes.x,f&&q(l.ticks,r),h&&q(l.minorTicks,r),l=this.axes.y,g&&q(l.ticks,s),i&&q(l.minorTicks,s),c.stroke();c.restore(),(f||h||g||i)&&a.fire(this.el,"flotr:aftergrid",[this.axes.x,this.axes.y,d,this])},drawOutline:function(){var a=this,b=a.options,c=b.grid,d=c.outline,e=a.ctx,f=c.backgroundImage,g=a.plotOffset,h=g.left,j=g.top,k=a.plotWidth,l=a.plotHeight,m,n,o,p,q,r;if(!c.outlineWidth)return;e.save();if(c.circular){e.translate(h+k/2,j+l/2);var s=Math.min(l,k)*b.radar.radiusRatio/2,t=this.axes.x.ticks.length,u=2*(Math.PI/t),v=-Math.PI/2;e.beginPath(),e.lineWidth=c.outlineWidth,e.strokeStyle=c.color,e.lineJoin="round";for(i=0;i<=t;++i)e[i===0?"moveTo":"lineTo"](Math.cos(i*u+v)*s,Math.sin(i*u+v)*s);e.stroke()}else{e.translate(h,j);var w=c.outlineWidth,x=.5-w+(w+1)%2/2,y="lineTo",z="moveTo";e.lineWidth=w,e.strokeStyle=c.color,e.lineJoin="miter",e.beginPath(),e.moveTo(x,x),k-=w/2%1,l+=w/2,e[d.indexOf("n")!==-1?y:z](k,x),e[d.indexOf("e")!==-1?y:z](k,l),e[d.indexOf("s")!==-1?y:z](x,l),e[d.indexOf("w")!==-1?y:z](x,x),e.stroke(),e.closePath()}e.restore(),f&&(o=f.src||f,p=(parseInt(f.left,10)||0)+g.left,q=(parseInt(f.top,10)||0)+g.top,n=new Image,n.onload=function(){e.save(),f.alpha&&(e.globalAlpha=f.alpha),e.globalCompositeOperation="destination-over",e.drawImage(n,0,0,n.width,n.height,p,q,k,l),e.restore()},n.src=o)}})}(),function(){var a=Flotr.DOM,b=Flotr._,c=Flotr,d="opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;";Flotr.addPlugin("hit",{callbacks:{"flotr:mousemove":function(a,b){this.hit.track(b)},"flotr:click":function(a){var c=this.hit.track(a);b.defaults(a,c)},"flotr:mouseout":function(){this.hit.clearHit()},"flotr:destroy":function(){this.mouseTrack=null}},track:function(a){if(this.options.mouse.track||b.any(this.series,function(a){return a.mouse&&a.mouse.track}))return this.hit.hit(a)},executeOnType:function(a,d,e){function h(a,h){b.each(b.keys(c.graphTypes),function(b){a[b]&&a[b].show&&this[b][d]&&(g=this.getOptions(a,b),g.fill=!!a.mouse.fillColor,g.fillStyle=this.processColor(a.mouse.fillColor||"#ffffff",{opacity:a.mouse.fillOpacity}),g.color=a.mouse.lineColor,g.context=this.octx,g.index=h,e&&(g.args=e),this[b][d].call(this[b],g),f=!0)},this)}var f=!1,g;return b.isArray(a)||(a=[a]),b.each(a,h,this),f},drawHit:function(a){var b=this.octx,c=a.series;if(c.mouse.lineColor){b.save(),b.lineWidth=c.points?c.points.lineWidth:1,b.strokeStyle=c.mouse.lineColor,b.fillStyle=this.processColor(c.mouse.fillColor||"#ffffff",{opacity:c.mouse.fillOpacity}),b.translate(this.plotOffset.left,this.plotOffset.top);if(!this.hit.executeOnType(c,"drawHit",a)){var d=a.xaxis,e=a.yaxis;b.beginPath(),b.arc(d.d2p(a.x),e.d2p(a.y),c.points.hitRadius||c.points.radius||c.mouse.radius,0,2*Math.PI,!0),b.fill(),b.stroke(),b.closePath()}b.restore(),this.clip(b)}this.prevHit=a},clearHit:function(){var b=this.prevHit,c=this.octx,d=this.plotOffset;c.save(),c.translate(d.left,d.top);if(b){if(!this.hit.executeOnType(b.series,"clearHit",this.prevHit)){var e=b.series,f=e.points?e.points.lineWidth:1;offset=(e.points.hitRadius||e.points.radius||e.mouse.radius)+f,c.clearRect(b.xaxis.d2p(b.x)-offset,b.yaxis.d2p(b.y)-offset,offset*2,offset*2)}a.hide(this.mouseTrack),this.prevHit=null}c.restore()},hit:function(a){var c=this.options,d=this.prevHit,e,f,g,h,i,j,k,l,m;if(this.series.length===0)return;m={relX:a.relX,relY:a.relY,absX:a.absX,absY:a.absY};if(c.mouse.trackY&&!c.mouse.trackAll&&this.hit.executeOnType(this.series,"hit",[a,m])&&!b.isUndefined(m.seriesIndex))i=this.series[m.seriesIndex],m.series=i,m.mouse=i.mouse,m.xaxis=i.xaxis,m.yaxis=i.yaxis;else{e=this.hit.closest(a);if(e){e=c.mouse.trackY?e.point:e.x,h=e.seriesIndex,i=this.series[h],k=i.xaxis,l=i.yaxis,f=2*i.mouse.sensibility;if(c.mouse.trackAll||e.distanceXk.xaxis.max)continue;n=Math.abs(r-p),o=Math.abs(s-q),m=n*n+o*o,m'),this.mouseTrack=k,a.insert(this.el,k));if(!b.mouse.relative)f.charAt(0)=="n"?c+="top:"+(g+p)+"px;bottom:auto;":f.charAt(0)=="s"&&(c+="bottom:"+(g+o)+"px;top:auto;"),f.charAt(1)=="e"?c+="right:"+(g+n)+"px;left:auto;":f.charAt(1)=="w"&&(c+="left:"+(g+m)+"px;right:auto;");else if(e.pie&&e.pie.show){var s={x:this.plotWidth/2,y:this.plotHeight/2},t=Math.min(this.canvasWidth,this.canvasHeight)*e.pie.sizeRatio/2,u=b.sAngle=5||Math.abs(a.second.y-a.first.y)>=5}})}(),function(){var a=Flotr.DOM;Flotr.addPlugin("labels",{callbacks:{"flotr:afterdraw":function(){this.labels.draw()}},draw:function(){function s(a,b,d){var e=d?b.minorTicks:b.ticks,f=b.orientation===1,h=b.n===1,k,m;k={color:b.options.color||o.grid.color,angle:Flotr.toRad(b.options.labelsAngle),textBaseline:"middle"};for(l=0;l(f?a.plotWidth:a.plotHeight))continue;Flotr.drawText(p,c.label,k(a,f,g,i),m(a,f,g,i),h),!f&&!g&&(p.save(),p.strokeStyle=h.color,p.beginPath(),p.moveTo(a.plotOffset.left+a.plotWidth-8,a.plotOffset.top+b.d2p(c.v)),p.lineTo(a.plotOffset.left+a.plotWidth,a.plotOffset.top+b.d2p(c.v)),p.stroke(),p.restore())}}function u(a,b){var d=b.orientation===1,e=b.n===1,g="",h,i,j,k=a.plotOffset;!d&&!e&&(p.save(),p.strokeStyle=b.options.color||o.grid.color,p.beginPath());if(b.options.showLabels&&(e?!0:b.used))for(l=0;l(d?a.canvasWidth:a.canvasHeight))continue;j=k.top+(d?(e?1:-1)*(a.plotHeight+o.grid.labelMargin):b.d2p(c.v)-b.maxLabel.height/2),h=d?k.left+b.d2p(c.v)-f/2:0,g="",l===0?g=" first":l===b.ticks.length-1&&(g=" last"),g+=d?" flotr-grid-label-x":" flotr-grid-label-y",m+=['
'+c.label+"
"].join(" "),!d&&!e&&(p.moveTo(k.left+a.plotWidth-8,k.top+b.d2p(c.v)),p.lineTo(k.left+a.plotWidth,k.top+b.d2p(c.v)))}}var b,c,d,e,f,g,h,i,j,k,l,m="",n=0,o=this.options,p=this.ctx,q=this.axes,r={size:o.fontSize};for(l=0;l-1;--n){if(!c[n].label||c[n].hide)continue;o=f.labelFormatter(c[n].label),v=Math.max(v,this._text.measureText(o,w).width)}var x=Math.round(q+s*3+v),y=Math.round(j*(s+r)+s);!m&&m!==0&&(m=.1);if(!e.HtmlText&&this.textEnabled&&!f.container){k.charAt(0)=="s"&&(u=d.top+this.plotHeight-(l+y)),k.charAt(0)=="c"&&(u=d.top+this.plotHeight/2-(l+y/2)),k.charAt(1)=="e"&&(t=d.left+this.plotWidth-(l+x)),p=this.processColor(f.backgroundColor,{opacity:m}),i.fillStyle=p,i.fillRect(t,u,x,y),i.strokeStyle=f.labelBoxBorderColor,i.strokeRect(Flotr.toPixel(t),Flotr.toPixel(u),x,y);var z=t+s,A=u+s;for(n=0;n":""),h=!0);var B=c[n],C=f.labelBoxWidth,E=f.labelBoxHeight;o=f.labelFormatter(B.label),p="background-color:"+(B.bars&&B.bars.show&&B.bars.fillColor&&B.bars.fill?B.bars.fillColor:B.color)+";",g.push('','
','
','
',"
","
","",'',o,"")}h&&g.push("");if(g.length>0){var F=''+g.join("")+"
";if(f.container)F=a.node(F),this.legend.markup=F,a.insert(f.container,F);else{var G={position:"absolute",zIndex:"2",border:"1px solid "+f.labelBoxBorderColor};k.charAt(0)=="n"?(G.top=l+d.top+"px",G.bottom="auto"):k.charAt(0)=="c"?(G.top=l+(this.plotHeight-y)/2+"px",G.bottom="auto"):k.charAt(0)=="s"&&(G.bottom=l+d.bottom+"px",G.top="auto"),k.charAt(1)=="e"?(G.right=l+d.right+"px",G.left="auto"):k.charAt(1)=="w"&&(G.left=l+d.left+"px",G.right="auto");var H=a.create("div"),I;H.className="flotr-legend",a.setStyles(H,G),a.insert(H,F),a.insert(this.el,H);if(!m)return;var J=f.backgroundColor||e.grid.backgroundColor||"#ffffff";b.extend(G,a.size(H),{backgroundColor:J,zIndex:"",border:""}),G.width+="px",G.height+="px",H=a.create("div"),H.className="flotr-legend-bg",a.setStyles(H,G),a.opacity(H,m),a.insert(H," "),a.insert(this.el,H)}}}}}})}(),function(){function a(a){if(this.options.spreadsheet.tickFormatter)return this.options.spreadsheet.tickFormatter(a);var b=c.find(this.axes.x.ticks,function(b){return b.v==a});return b?b.label:a}var b=Flotr.DOM,c=Flotr._;Flotr.addPlugin("spreadsheet",{options:{show:!1,tabGraphLabel:"Graph",tabDataLabel:"Data",toolbarDownload:"Download CSV",toolbarSelectAll:"Select all",csvFileSeparator:",",decimalSeparator:".",tickFormatter:null,initialTab:"graph"},callbacks:{"flotr:afterconstruct":function(){if(!this.options.spreadsheet.show)return;var a=this.spreadsheet,c=b.node('
'),d=b.node('
'+this.options.spreadsheet.tabGraphLabel+"
"),e=b.node('
'+this.options.spreadsheet.tabDataLabel+"
"),f;a.tabsContainer=c,a.tabs={graph:d,data:e},b.insert(c,d),b.insert(c,e),b.insert(this.el,c),f=b.size(e).height+2,this.plotOffset.bottom+=f,b.setStyles(c,{top:this.canvasHeight-f+"px"}),this.observe(d,"click",function(){a.showTab("graph")}).observe(e,"click",function(){a.showTab("data")}),this.options.spreadsheet.initialTab!=="graph"&&a.showTab(this.options.spreadsheet.initialTab)}},loadDataGrid:function(){if(this.seriesData)return this.seriesData;var a=this.series,b={};return c.each(a,function(a,d){c.each(a.data,function(a){var c=a[0],e=a[1],f=b[c];if(f)f[d+1]=e;else{var g=[];g[0]=c,g[d+1]=e,b[c]=g}})}),this.seriesData=c.sortBy(b,function(a,b){return parseInt(b,10)}),this.seriesData},constructDataGrid:function(){if(this.spreadsheet.datagrid)return this.spreadsheet.datagrid;var d=this.series,e=this.spreadsheet.loadDataGrid(),f=[""],g,h,i,j=[''];j.push(""),c.each(d,function(a,b){j.push('"),f.push("")}),j.push(""),c.each(e,function(b){j.push(""),c.times(d.length+1,function(d){var e="td",f=b[d],g=c.isUndefined(f)?"":Math.round(f*1e5)/1e5;if(d===0){e="th";var h=a.call(this,g);h&&(g=h)}j.push("<"+e+(e=="th"?' scope="row"':"")+">"+g+"")},this),j.push("")},this),f.push(""),i=b.node(j.join("")),g=b.node('"),h=b.node('"),this.observe(g,"click",c.bind(this.spreadsheet.downloadCSV,this)).observe(h,"click",c.bind(this.spreadsheet.selectAllData,this));var k=b.node('
');b.insert(k,g),b.insert(k,h);var l=this.canvasHeight-b.size(this.spreadsheet.tabsContainer).height-2,m=b.node('
');return b.insert(m,k),b.insert(m,i),b.insert(this.el,m),this.spreadsheet.datagrid=i,this.spreadsheet.container=m,i},showTab:function(a){if(this.spreadsheet.activeTab===a)return;switch(a){case"graph":b.hide(this.spreadsheet.container),b.removeClass(this.spreadsheet.tabs.data,"selected"),b.addClass(this.spreadsheet.tabs.graph,"selected");break;case"data":this.spreadsheet.datagrid||this.spreadsheet.constructDataGrid(),b.show(this.spreadsheet.container),b.addClass(this.spreadsheet.tabs.data,"selected"),b.removeClass(this.spreadsheet.tabs.graph,"selected");break;default:throw"Illegal tab name: "+a}this.spreadsheet.activeTab=a},selectAllData:function(){if(this.spreadsheet.tabs){var a,b,c,d,e=this.spreadsheet.constructDataGrid();return this.spreadsheet.showTab("data"),setTimeout(function(){(c=e.ownerDocument)&&(d=c.defaultView)&&d.getSelection&&c.createRange&&(a=window.getSelection())&&a.removeAllRanges?(b=c.createRange(),b.selectNode(e),a.removeAllRanges(),a.addRange(b)):document.body&&document.body.createTextRange&&(b=document.body.createTextRange())&&(b.moveToElementText(e),b.select())},0),!0}return!1},downloadCSV:function(){var b="",d=this.series,e=this.options,f=this.spreadsheet.loadDataGrid(),g=encodeURIComponent(e.spreadsheet.csvFileSeparator);if(e.spreadsheet.decimalSeparator===e.spreadsheet.csvFileSeparator)throw"The decimal separator is the same as the column separator ("+e.spreadsheet.decimalSeparator+")";c.each(d,function(a,c){b+=g+'"'+(a.label||String.fromCharCode(65+c)).replace(/\"/g,'\\"')+'"'}),b+="%0D%0A",b+=c.reduce(f,function(b,c){var d=a.call(this,c[0])||"";d='"'+(d+"").replace(/\"/g,'\\"')+'"';var f=c.slice(1).join(g);return e.spreadsheet.decimalSeparator!=="."&&(f=f.replace(/\./g,e.spreadsheet.decimalSeparator)),b+d+g+f+"%0D%0A"},"",this),Flotr.isIE&&Flotr.isIE<9?(b=b.replace(new RegExp(g,"g"),decodeURIComponent(g)).replace(/%0A/g,"\n").replace(/%0D/g,"\r"),window.open().document.write(b)):window.open("data:text/csv,"+b)}})}(),function(){var a=Flotr.DOM;Flotr.addPlugin("titles",{callbacks:{"flotr:afterdraw":function(){this.titles.drawTitles()}},drawTitles:function(){var b,c=this.options,d=c.grid.labelMargin,e=this.ctx,f=this.axes;if(!c.HtmlText&&this.textEnabled){var g={size:c.fontSize,color:c.grid.color,textAlign:"center"};c.subtitle&&Flotr.drawText(e,c.subtitle,this.plotOffset.left+this.plotWidth/2,this.titleHeight+this.subtitleHeight-2,g),g.weight=1.5,g.size*=1.5,c.title&&Flotr.drawText(e,c.title,this.plotOffset.left+this.plotWidth/2,this.titleHeight-2,g),g.weight=1.8,g.size*=.8,f.x.options.title&&f.x.used&&(g.textAlign=f.x.options.titleAlign||"center",g.textBaseline="top",g.angle=Flotr.toRad(f.x.options.titleAngle),g=Flotr.getBestTextAlign(g.angle,g),Flotr.drawText(e,f.x.options.title,this.plotOffset.left+this.plotWidth/2,this.plotOffset.top+f.x.maxLabel.height+this.plotHeight+2*d,g)),f.x2.options.title&&f.x2.used&&(g.textAlign=f.x2.options.titleAlign||"center",g.textBaseline="bottom",g.angle=Flotr.toRad(f.x2.options.titleAngle),g=Flotr.getBestTextAlign(g.angle,g),Flotr.drawText(e,f.x2.options.title,this.plotOffset.left+this.plotWidth/2,this.plotOffset.top-f.x2.maxLabel.height-2*d,g)),f.y.options.title&&f.y.used&&(g.textAlign=f.y.options.titleAlign||"right",g.textBaseline="middle",g.angle=Flotr.toRad(f.y.options.titleAngle),g=Flotr.getBestTextAlign(g.angle,g),Flotr.drawText(e,f.y.options.title,this.plotOffset.left-f.y.maxLabel.width-2*d,this.plotOffset.top+this.plotHeight/2,g)),f.y2.options.title&&f.y2.used&&(g.textAlign=f.y2.options.titleAlign||"left",g.textBaseline="middle",g.angle=Flotr.toRad(f.y2.options.titleAngle),g=Flotr.getBestTextAlign(g.angle,g),Flotr.drawText(e,f.y2.options.title,this.plotOffset.left+this.plotWidth+f.y2.maxLabel.width+2*d,this.plotOffset.top+this.plotHeight/2,g))}else{b=[],c.title&&b.push('
',c.title,"
"),c.subtitle&&b.push('
',c.subtitle,"
"),b.push(""),b.push('
'),f.x.options.title&&f.x.used&&b.push('
',f.x.options.title,"
"),f.x2.options.title&&f.x2.used&&b.push('
',f.x2.options.title,"
"),f.y.options.title&&f.y.used&&b.push('
',f.y.options.title,"
"),f.y2.options.title&&f.y2.used&&b.push('
',f.y2.options.title,"
"),b=b.join("");var h=a.create("div");a.setStyles({color:c.grid.color}),h.className="flotr-titles",a.insert(this.el,h),a.insert(h,b)}}})}(); diff --git a/pybossa/static/js/stats/leaflet.markercluster.js b/pybossa/static/js/stats/leaflet.markercluster.js new file mode 100644 index 0000000000..6745ef690b --- /dev/null +++ b/pybossa/static/js/stats/leaflet.markercluster.js @@ -0,0 +1,6 @@ +/* + Copyright (c) 2012, Smartrak, David Leaver + Leaflet.markercluster is an open-source JavaScript library for Marker Clustering on leaflet powered maps. + https://github.com/danzel/Leaflet.markercluster +*/ +(function(e,t){L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,animateAddingMarkers:!1,polygonOptions:{}},initialize:function(e){L.Util.setOptions(this,e),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),L.FeatureGroup.prototype.initialize.call(this,[]),this._inZoomAnimation=0,this._needsClustering=[],this._currentShownBounds=null},addLayer:function(e){if(e instanceof L.LayerGroup){var t=[];for(var n in e._layers)e._layers.hasOwnProperty(n)&&t.push(e._layers[n]);return this.addLayers(t)}if(!this._map)return this._needsClustering.push(e),this;if(this.hasLayer(e))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(e,this._maxZoom);var r=e,i=this._map.getZoom();if(e.__parent)while(r.__parent._zoom>=i)r=r.__parent;return this._currentShownBounds.contains(r.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(e,r):this._animationAddLayerNonAnimated(e,r)),this},removeLayer:function(e){return this._map?e.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(e)),this._removeLayer(e,!0),e._icon&&(L.FeatureGroup.prototype.removeLayer.call(this,e),e.setOpacity(1)),delete e.__parent,this):this:(this._arraySplice(this._needsClustering,e),this)},addLayers:function(e){if(!this._map)return this._needsClustering=this._needsClustering.concat(e),this;for(var t=0,n=e.length;t=0;t--)e.extend(this._needsClustering[t].getLatLng());return e},hasLayer:function(e){if(this._needsClustering.length>0){var t=this._needsClustering;for(var n=t.length-1;n>=0;n--)if(t[n]===e)return!0}return!!e.__parent&&e.__parent._group===this},zoomToShowLayer:function(e,t){var n=function(){if((e._icon||e.__parent._icon)&&!this._inZoomAnimation){this._map.off("moveend",n,this),this.off("animationend",n,this);if(e._icon)t();else if(e.__parent._icon){var r=function(){this.off("spiderfied",r,this),t()};this.on("spiderfied",r,this),e.__parent.spiderfy()}}};e._icon?t():e.__parent._zoom=0;n--)if(e[n]===t){e.splice(n,1);return}},_removeLayer:function(e,t,n){var r=this._gridClusters,i=this._gridUnclustered,s=this._map;if(t)for(var o=this._maxZoom;o>=0;o--)if(!i[o].removeObject(e,s.project(e.getLatLng(),o)))break;var u=e.__parent,a=u._markers,f;this._arraySplice(a,e);while(u){u._childCount--;if(u._zoom<0)break;t&&u._childCount<=1?(f=u._markers[0]===e?u._markers[1]:u._markers[0],r[u._zoom].removeObject(u,s.project(u._cLatLng,u._zoom)),i[u._zoom].addObject(f,s.project(f.getLatLng(),u._zoom)),this._arraySplice(u.__parent._childClusters,u),u.__parent._markers.push(f),f.__parent=u.__parent,u._icon&&(L.FeatureGroup.prototype.removeLayer.call(this,u),n||L.FeatureGroup.prototype.addLayer.call(this,f))):(u._recalculateBounds(),(!n||!u._icon)&&u._updateIcon()),u=u.__parent}},_propagateEvent:function(e){e.target instanceof L.MarkerCluster&&(e.type="cluster"+e.type),L.FeatureGroup.prototype._propagateEvent.call(this,e)},_defaultIconCreateFunction:function(e){var t=e.getChildCount(),n=" marker-cluster-";return t<10?n+="small":t<100?n+="medium":n+="large",new L.DivIcon({html:"
"+t+"
",className:"marker-cluster"+n,iconSize:new L.Point(40,40)})},_bindEvents:function(){var e=null,t=this._map,n=this.options.spiderfyOnMaxZoom,r=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick;(n||i)&&this.on("clusterclick",function(e){t.getMaxZoom()===t.getZoom()?n&&e.layer.spiderfy():i&&e.layer.zoomToBounds()},this),r&&(this.on("clustermouseover",function(n){if(this._inZoomAnimation)return;e&&t.removeLayer(e),n.layer.getChildCount()>2&&(e=new L.Polygon(n.layer.getConvexHull(),this.options.polygonOptions),t.addLayer(e))},this),this.on("clustermouseout",function(){e&&(t.removeLayer(e),e=null)},this),t.on("zoomend",function(){e&&(t.removeLayer(e),e=null)},this),t.on("layerremove",function(n){e&&n.layer===this&&(t.removeLayer(e),e=null)},this))},_zoomEnd:function(){if(!this._map)return;this._mergeSplitClusters(),this._zoom=this._map._zoom,this._currentShownBounds=this._getExpandedVisibleBounds()},_moveEnd:function(){if(this._inZoomAnimation)return;var e=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,this._zoom,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,e),this._currentShownBounds=e;return},_generateInitialClusters:function(){var e=this._map.getMaxZoom(),t=this.options.maxClusterRadius;this.options.disableClusteringAtZoom&&(e=this.options.disableClusteringAtZoom-1),this._maxZoom=e,this._gridClusters={},this._gridUnclustered={};for(var n=e;n>=0;n--)this._gridClusters[n]=new L.DistanceGrid(t),this._gridUnclustered[n]=new L.DistanceGrid(t);this._topClusterLevel=new L.MarkerCluster(this,-1)},_addLayer:function(e,t){var n=this._gridClusters,r=this._gridUnclustered,i,s;this.options.singleMarkerMode&&(e.options.icon=this.options.iconCreateFunction({getChildCount:function(){return 1},getAllChildMarkers:function(){return[e]}}));for(;t>=0;t--){i=this._map.project(e.getLatLng(),t);var o=n[t].getNearObject(i);if(o){o._addChild(e),e.__parent=o;return}o=r[t].getNearObject(i);if(o){o.__parent&&this._removeLayer(o,!1);var u=o.__parent,a=new L.MarkerCluster(this,t,o,e);n[t].addObject(a,this._map.project(a._cLatLng,t)),o.__parent=a,e.__parent=a;var f=a;for(s=t-1;s>u._zoom;s--)f=new L.MarkerCluster(this,s,f),n[s].addObject(f,this._map.project(o.getLatLng(),s));u._addChild(f);for(s=t;s>=0;s--)if(!r[s].removeObject(o,this._map.project(o.getLatLng(),s)))break;return}r[t].addObject(e,i)}this._topClusterLevel._addChild(e),e.__parent=this._topClusterLevel;return},_mergeSplitClusters:function(){this._zoomthis._map._zoom?(this._animationStart(),this._animationZoomOut(this._zoom,this._map._zoom)):this._moveEnd()},_getExpandedVisibleBounds:function(){var e=this._map,t=e.getBounds(),n=t._southWest,r=t._northEast,i=L.Browser.mobile?0:Math.abs(n.lat-r.lat),s=L.Browser.mobile?0:Math.abs(n.lng-r.lng);return new L.LatLngBounds(new L.LatLng(n.lat-i,n.lng-s,!0),new L.LatLng(r.lat+i,r.lng+s,!0))},_animationAddLayerNonAnimated:function(e,t){if(t===e)L.FeatureGroup.prototype.addLayer.call(this,e);else if(t._childCount===2){t._addToMap();var n=t.getAllChildMarkers();L.FeatureGroup.prototype.removeLayer.call(this,n[0]),L.FeatureGroup.prototype.removeLayer.call(this,n[1])}else t._updateIcon()}}),L.MarkerClusterGroup.include(L.DomUtil.TRANSITION?{_animationStart:function(){this._map._mapPane.className+=" leaflet-cluster-anim",this._inZoomAnimation++},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_animationZoomIn:function(e,t){var n=this,r=this._getExpandedVisibleBounds(),i;this._topClusterLevel._recursively(r,e,0,function(s){var o=s._latlng,u=s._markers,a;s._isSingleParent()&&e+1===t?(L.FeatureGroup.prototype.removeLayer.call(n,s),s._recursivelyAddChildrenToMap(null,t,r)):(s.setOpacity(0),s._recursivelyAddChildrenToMap(o,t,r));for(i=u.length-1;i>=0;i--)a=u[i],r.contains(a._latlng)||L.FeatureGroup.prototype.removeLayer.call(n,a)}),this._forceLayout();var s,o;n._topClusterLevel._recursivelyBecomeVisible(r,t);for(s in n._layers)n._layers.hasOwnProperty(s)&&(o=n._layers[s],!(o instanceof L.MarkerCluster)&&o._icon&&o.setOpacity(1));n._topClusterLevel._recursively(r,e,t,function(e){e._recursivelyRestoreChildPositions(t)}),setTimeout(function(){n._topClusterLevel._recursively(r,e,0,function(e){L.FeatureGroup.prototype.removeLayer.call(n,e),e.setOpacity(1)}),n._animationEnd()},250)},_animationZoomOut:function(e,t){this._animationZoomOutSingle(this._topClusterLevel,e-1,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,e,this._getExpandedVisibleBounds())},_animationZoomOutSingle:function(e,t,n){var r=this._getExpandedVisibleBounds();e._recursivelyAnimateChildrenInAndAddSelfToMap(r,t+1,n);var i=this;this._forceLayout(),e._recursivelyBecomeVisible(r,n),setTimeout(function(){if(e._childCount===1){var s=e._markers[0];s.setLatLng(s.getLatLng()),s.setOpacity(1);return}e._recursively(r,n,0,function(e){e._recursivelyRemoveChildrenFromMap(r,t+1)}),i._animationEnd()},250)},_animationAddLayer:function(e,t){var n=this;L.FeatureGroup.prototype.addLayer.call(this,e),t!==e&&(t._childCount>2?(t._updateIcon(),this._forceLayout(),this._animationStart(),e._setPos(this._map.latLngToLayerPoint(t.getLatLng())),e.setOpacity(0),setTimeout(function(){L.FeatureGroup.prototype.removeLayer.call(n,e),e.setOpacity(1),n._animationEnd()},250)):(this._forceLayout(),n._animationStart(),n._animationZoomOutSingle(t,this._map.getMaxZoom(),this._map.getZoom())))},_forceLayout:function(){L.Util.falseFn(document.body.offsetWidth)}}:{_animationStart:function(){},_animationZoomIn:function(e,t){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds())},_animationZoomOut:function(e,t){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds())},_animationAddLayer:function(e,t){this._animationAddLayerNonAnimated(e,t)}}),L.MarkerCluster=L.Marker.extend({initialize:function(e,t,n,r){L.Marker.prototype.initialize.call(this,n?n._cLatLng||n.getLatLng():new L.LatLng(0,0),{icon:this}),this._group=e,this._zoom=t,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._bounds=new L.LatLngBounds,n&&this._addChild(n),r&&this._addChild(r)},getAllChildMarkers:function(e){e=e||[];for(var t=this._childClusters.length-1;t>=0;t--)this._childClusters[t].getAllChildMarkers(e);for(var n=this._markers.length-1;n>=0;n--)e.push(this._markers[n]);return e},getChildCount:function(){return this._childCount},zoomToBounds:function(){this._group._map.fitBounds(this._bounds)},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(e,t){this._iconNeedsUpdate=!0,this._expandBounds(e),e instanceof L.MarkerCluster?(t||(this._childClusters.push(e),e.__parent=this),this._childCount+=e._childCount):(t||this._markers.push(e),this._childCount++),this.__parent&&this.__parent._addChild(e,!0)},_expandBounds:function(e){var t,n=e._wLatLng||e._latlng;e instanceof L.MarkerCluster?(this._bounds.extend(e._bounds),t=e._childCount):(this._bounds.extend(n),t=1),this._cLatLng||(this._cLatLng=e._cLatLng||n);var r=this._childCount+t;this._wLatLng?(this._wLatLng.lat=(n.lat*t+this._wLatLng.lat*this._childCount)/r,this._wLatLng.lng=(n.lng*t+this._wLatLng.lng*this._childCount)/r):this._latlng=this._wLatLng=new L.LatLng(n.lat,n.lng)},_addToMap:function(e){e&&(this._backupLatlng=this._latlng,this.setLatLng(e)),L.FeatureGroup.prototype.addLayer.call(this._group,this)},_recursivelyAnimateChildrenIn:function(e,t,n){this._recursively(e,0,n-1,function(e){var n=e._markers,r,i;for(r=n.length-1;r>=0;r--)i=n[r],i._icon&&(i._setPos(t),i.setOpacity(0))},function(e){var n=e._childClusters,r,i;for(r=n.length-1;r>=0;r--)i=n[r],i._icon&&(i._setPos(t),i.setOpacity(0))})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(e,t,n){this._recursively(e,n,0,function(r){r._recursivelyAnimateChildrenIn(e,r._group._map.latLngToLayerPoint(r.getLatLng()).round(),t),r._isSingleParent()&&t-1===n?(r.setOpacity(1),r._recursivelyRemoveChildrenFromMap(e,t)):r.setOpacity(0),r._addToMap()})},_recursivelyBecomeVisible:function(e,t){this._recursively(e,0,t,null,function(e){e.setOpacity(1)})},_recursivelyAddChildrenToMap:function(e,t,n){this._recursively(n,-1,t,function(r){if(t===r._zoom)return;for(var i=r._markers.length-1;i>=0;i--){var s=r._markers[i];if(!n.contains(s._latlng))continue;e&&(s._backupLatlng=s.getLatLng(),s.setLatLng(e),s.setOpacity(0)),L.FeatureGroup.prototype.addLayer.call(r._group,s)}},function(t){t._addToMap(e)})},_recursivelyRestoreChildPositions:function(e){for(var t=this._markers.length-1;t>=0;t--){var n=this._markers[t];n._backupLatlng&&(n.setLatLng(n._backupLatlng),delete n._backupLatlng)}if(e-1===this._zoom)for(var r=this._childClusters.length-1;r>=0;r--)this._childClusters[r]._restorePosition();else for(var i=this._childClusters.length-1;i>=0;i--)this._childClusters[i]._recursivelyRestoreChildPositions(e)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(e,t,n){var r,i;this._recursively(e,-1,t-1,function(e){for(i=e._markers.length-1;i>=0;i--){r=e._markers[i];if(!n||!n.contains(r._latlng))L.FeatureGroup.prototype.removeLayer.call(e._group,r),r.setOpacity(1)}},function(e){for(i=e._childClusters.length-1;i>=0;i--){r=e._childClusters[i];if(!n||!n.contains(r._latlng))L.FeatureGroup.prototype.removeLayer.call(e._group,r),r.setOpacity(1)}})},_recursively:function(e,t,n,r,i){var s=this._childClusters,o=this._zoom,u,a;if(t>o)for(u=s.length-1;u>=0;u--)a=s[u],e.intersects(a._bounds)&&a._recursively(e,t,n,r,i);else{r&&r(this),i&&this._zoom===n&&i(this);if(n>o)for(u=s.length-1;u>=0;u--)a=s[u],e.intersects(a._bounds)&&a._recursively(e,t,n,r,i)}},_recalculateBounds:function(){var e=this._markers,t=this._childClusters,n;this._bounds=new L.LatLngBounds,delete this._wLatLng;for(n=e.length-1;n>=0;n--)this._expandBounds(e[n]);for(n=t.length-1;n>=0;n--)this._expandBounds(t[n])},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}}),L.DistanceGrid=function(e){this._cellSize=e,this._sqCellSize=e*e,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(e,t){var n=this._getCoord(t.x),r=this._getCoord(t.y),i=this._grid,s=i[r]=i[r]||{},o=s[n]=s[n]||[],u=L.Util.stamp(e);this._objectPoint[u]=t,o.push(e)},updateObject:function(e,t){this.removeObject(e),this.addObject(e,t)},removeObject:function(e,t){var n=this._getCoord(t.x),r=this._getCoord(t.y),i=this._grid,s=i[r]=i[r]||{},o=s[n]=s[n]||[],u,a;delete this._objectPoint[L.Util.stamp(e)];for(u=0,a=o.length;u=0;s--){o=t[s],u=this.getDistant(o,e);if(!(u>0))continue;i.push(o),u>n&&(n=u,r=o)}return{maxPoint:r,newPoints:i}},buildConvexHull:function(e,t){var n=[],r=this.findMostDistantPointFromBaseLine(e,t);return r.maxPoint?(n=n.concat(this.buildConvexHull([e[0],r.maxPoint],r.newPoints)),n=n.concat(this.buildConvexHull([r.maxPoint,e[1]],r.newPoints)),n):[e]},getConvexHull:function(e){var t=!1,n=!1,r=null,i=null,s;for(s=e.length-1;s>=0;s--){var o=e[s];if(t===!1||o.lat>t)r=o,t=o.lat;if(n===!1||o.lat=0;s--)i=e[s].getLatLng(),t.push(i);r=L.QuickHull.getConvexHull(t);for(s=r.length-1;s>=0;s--)n.push(r[s][0]);return n}}),L.MarkerCluster.include({_2PI:Math.PI*2,_circleFootSeparation:25,_circleStartAngle:Math.PI/6,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied===this||this._group._inZoomAnimation)return;var e=this.getAllChildMarkers(),t=this._group,n=t._map,r=n.latLngToLayerPoint(this._latlng),i;this._group._unspiderfy(),this._group._spiderfied=this,e.length>=this._circleSpiralSwitchover?i=this._generatePointsSpiral(e.length,r):(r.y+=10,i=this._generatePointsCircle(e.length,r)),this._animationSpiderfy(e,i)},unspiderfy:function(e){if(this._group._inZoomAnimation)return;this._animationUnspiderfy(e),this._group._spiderfied=null},_generatePointsCircle:function(e,t){var n=this._circleFootSeparation*(2+e),r=n/this._2PI,i=this._2PI/e,s=[],o,u;s.length=e;for(o=e-1;o>=0;o--)u=this._circleStartAngle+o*i,s[o]=(new L.Point(t.x+r*Math.cos(u),t.y+r*Math.sin(u)))._round();return s},_generatePointsSpiral:function(e,t){var n=this._spiralLengthStart,r=0,i=[],s;i.length=e;for(s=e-1;s>=0;s--)r+=this._spiralFootSeparation/n+s*5e-4,i[s]=(new L.Point(t.x+n*Math.cos(r),t.y+n*Math.sin(r)))._round(),n+=this._2PI*this._spiralLengthFactor/r;return i}}),L.MarkerCluster.include(L.DomUtil.TRANSITION?{_animationSpiderfy:function(e,t){var n=this,r=this._group,i=r._map,s=i.latLngToLayerPoint(this._latlng),o,u,a,f;for(o=e.length-1;o>=0;o--)u=e[o],u.setZIndexOffset(1e6),u.setOpacity(0),L.FeatureGroup.prototype.addLayer.call(r,u),u._setPos(s);r._forceLayout(),r._animationStart();var l=L.Path.SVG?0:.3,c=L.Path.SVG_NS;for(o=e.length-1;o>=0;o--){f=i.layerPointToLatLng(t[o]),u=e[o],u._preSpiderfyLatlng=u._latlng,u.setLatLng(f),u.setOpacity(1),a=new L.Polyline([n._latlng,f],{weight:1.5,color:"#222",opacity:l}),i.addLayer(a),u._spiderLeg=a;if(!L.Path.SVG)continue;var h=a._path.getTotalLength();a._path.setAttribute("stroke-dasharray",h+","+h);var p=document.createElementNS(c,"animate");p.setAttribute("attributeName","stroke-dashoffset"),p.setAttribute("begin","indefinite"),p.setAttribute("from",h),p.setAttribute("to",0),p.setAttribute("dur",.25),a._path.appendChild(p),p.beginElement(),p=document.createElementNS(c,"animate"),p.setAttribute("attributeName","stroke-opacity"),p.setAttribute("attributeName","stroke-opacity"),p.setAttribute("begin","indefinite"),p.setAttribute("from",0),p.setAttribute("to",.5),p.setAttribute("dur",.25),a._path.appendChild(p),p.beginElement()}n.setOpacity(.3);if(L.Path.SVG){this._group._forceLayout();for(o=e.length-1;o>=0;o--)u=e[o]._spiderLeg,u.options.opacity=.5,u._path.setAttribute("stroke-opacity",.5)}setTimeout(function(){r._animationEnd(),r.fire("spiderfied")},250)},_animationUnspiderfy:function(e){var t=this._group,n=t._map,r=e?n._latLngToNewLayerPoint(this._latlng,e.zoom,e.center):n.latLngToLayerPoint(this._latlng),i=this.getAllChildMarkers(),s=L.Path.SVG,o,u,a;t._animationStart(),this.setOpacity(1);for(u=i.length-1;u>=0;u--)o=i[u],o.setLatLng(o._preSpiderfyLatlng),delete o._preSpiderfyLatlng,o._setPos(r),o.setOpacity(0),s&&(a=o._spiderLeg._path.childNodes[0],a.setAttribute("to",a.getAttribute("from")),a.setAttribute("from",0),a.beginElement(),a=o._spiderLeg._path.childNodes[1],a.setAttribute("from",.5),a.setAttribute("to",0),a.setAttribute("stroke-opacity",0),a.beginElement(),o._spiderLeg._path.setAttribute("stroke-opacity",0));setTimeout(function(){var e=0;for(u=i.length-1;u>=0;u--)o=i[u],o._spiderLeg&&e++;for(u=i.length-1;u>=0;u--){o=i[u];if(!o._spiderLeg)continue;o.setOpacity(1),o.setZIndexOffset(0),e>1&&L.FeatureGroup.prototype.removeLayer.call(t,o),n.removeLayer(o._spiderLeg),delete o._spiderLeg}t._animationEnd()},250)}}:{_animationSpiderfy:function(e,t){var n=this._group,r=n._map,i,s,o,u;for(i=e.length-1;i>=0;i--)u=r.layerPointToLatLng(t[i]),s=e[i],s._preSpiderfyLatlng=s._latlng,s.setLatLng(u),s.setZIndexOffset(1e6),L.FeatureGroup.prototype.addLayer.call(n,s),o=new L.Polyline([this._latlng,u],{weight:1.5,color:"#222"}),r.addLayer(o),s._spiderLeg=o;this.setOpacity(.3),n.fire("spiderfied")},_animationUnspiderfy:function(){var e=this._group,t=e._map,n=this.getAllChildMarkers(),r,i;this.setOpacity(1);for(i=n.length-1;i>=0;i--)r=n[i],L.FeatureGroup.prototype.removeLayer.call(e,r),r.setLatLng(r._preSpiderfyLatlng),delete r._preSpiderfyLatlng,r.setZIndexOffset(0),t.removeLayer(r._spiderLeg),delete r._spiderLeg}}),L.MarkerClusterGroup.include({_spiderfied:null,_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation?this._map.on("zoomstart",this._unspiderfyZoomStart,this):this._map.on("zoomend",this._unspiderfyWrapper,this),L.Path.SVG&&!L.Browser.touch&&this._map._initPathRoot()},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy()},_unspiderfyZoomStart:function(){if(!this._map)return;this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(e){if(L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching"))return;this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(e)},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(e){this._spiderfied&&this._spiderfied.unspiderfy(e)},_unspiderfyLayer:function(e){e._spiderLeg&&(L.FeatureGroup.prototype.removeLayer.call(this,e),e.setOpacity(1),e.setZIndexOffset(0),this._map.removeLayer(e._spiderLeg),delete e._spiderLeg)}})})(this); \ No newline at end of file diff --git a/pybossa/templates/applications/stats.html b/pybossa/templates/applications/stats.html new file mode 100644 index 0000000000..376719fd9f --- /dev/null +++ b/pybossa/templates/applications/stats.html @@ -0,0 +1,411 @@ +{% extends "/base.html" %} +{% set active_page = "applications" %} +{% set active_app = "all" %} +{% import "applications/_helpers.html" as helper %} + +{% block content %} + + + + + + + + + + + + +
+
+ {% if app.info.thumbnail %} +
+ +
+
+

Stats for application: {{app.name}}

+
+ {% else %} +
+

Stats for application: {{app.name}}

+
+ {% endif %} +
+ + + + + {% if private %} +
+
+ + Important: Data have been anonymized! +
+
+ {% endif %} + + +
+
+

Distribution of Answers per Hour

+
+
+ +
+
+ + + +
+
+

Total Answers per day select an area to zoom, click to reset the chart

+
+ +
+
+ + + + +
+
+

Answers per day select an area to zoom, click to reset the chart

+
+ +
+
+ + + + +
+
+

Distribution of Answers

+
+
+
 '+(a.label||String.fromCharCode(65+b))+"
+ + + + + + + + {% for k in userStats.keys() %} + + + + + + {% endfor %} +
UserTasksPercentage
{{k | capitalize}}{{userStats[k].taskruns}}{{userStats[k].pct_taskruns}} %
+ + +
+
+ + + + + + + +
+
+

Details about Anonymous Users

+
+
+

{{userStats.anonymous.users}} have participated.

+ {% if userStats.anonymous.top5 %} +

Top 5 users

+ + + + + + + + + + + {% for u in userStats.anonymous.top5 %} + + + + + + + + {% endfor %} +
#IP AddressCountryCityTasks
{{loop.index}}{{u.ip}}{{u.loc.country_name}}{{u.loc.city}}{{u.tasks}}
+ {% endif %} +
+
+
+ +
+
+
+
+
+ +
+
+
+ + + +
+
+

Details about Authenticated Users

+
+
+

{{userStats.authenticated.users}} have participated.

+ {% if userStats.authenticated.top5 %} +

Top 5 users

+ + + + + + + + + {% for u in userStats.authenticated.top5 %} + + + + + + {% endfor %} +
#PyBossa User IDTasks
{{loop.index}}{{u.id}}{{u.tasks}}
+ {% endif %} +
+
+
+ +
+
+
+ + +{% endblock %} diff --git a/pybossa/view/applications.py b/pybossa/view/applications.py index 4511bbc8eb..96d0ccfacf 100644 --- a/pybossa/view/applications.py +++ b/pybossa/view/applications.py @@ -792,10 +792,29 @@ def stats(short_name): app = db.session.query(model.App).filter_by(short_name=short_name).first() title="Application: %s · Stats" % app.name dates_stats, hours_stats, users_stats = cached_apps.get_stats(app.id) - print dates_stats - print hours_stats - print users_stats + anon_pct_taskruns = int((users_stats['n_anon']*100)/(users_stats['n_anon']+users_stats['n_auth'])) + userStats=dict( + anonymous=dict( + users=len(users_stats['anon']['values']), + taskruns=users_stats['n_anon'], + pct_taskruns=anon_pct_taskruns, + top5=users_stats['anon']['top5']), + authenticated=dict( + users=len(users_stats['auth']['values']), + taskruns=users_stats['n_auth'], + pct_taskruns=100-anon_pct_taskruns, + top5=users_stats['auth']['top5']), + ) + + tmp = dict(userStats=users_stats['users'], + userAnonStats=users_stats['anon'], + userAuthStats=users_stats['auth'], + dayStats=dates_stats, + hourStats=hours_stats) + + return render_template('/applications/stats.html', title=title, - userStats=None, + appStats=json.dumps(tmp), + userStats=userStats, app=app) From 9b54f9c32455975b629b3b4c27465ee99fa14ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lombra=C3=B1a=20Gonz=C3=A1lez?= Date: Wed, 13 Mar 2013 14:19:20 +0100 Subject: [PATCH 11/28] Include MaxMind PyGeoIP GeoLite license --- README.rst | 3 +-- pybossa/templates/applications/stats.html | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index efa66e2a61..389e3ad6a1 100644 --- a/README.rst +++ b/README.rst @@ -54,5 +54,4 @@ Authors * Twitter Bootstrap Icons by Glyphicons http://http://glyphicons.com/ * FontAwesome fonts by http://fortawesome.github.com/Font-Awesome/ - - +* GeoLite data by MaxMind http://www.maxmind.com diff --git a/pybossa/templates/applications/stats.html b/pybossa/templates/applications/stats.html index 376719fd9f..78e1c2b129 100644 --- a/pybossa/templates/applications/stats.html +++ b/pybossa/templates/applications/stats.html @@ -322,6 +322,8 @@

Top 5 users

+

This page includes GeoLite data created by MaxMind, available from + http://www.maxmind.com

+
+
+ {{helper.render_app_local_nav(app, 'stats', current_user)}} +
+
{% if app.info.thumbnail %} @@ -27,11 +32,11 @@
-

Stats for application: {{app.name}}

+

{{app.name}}: Stats

{% else %}
-

Stats for application: {{app.name}}

+

{{app.name}}: Stats

{% endif %}
@@ -271,10 +276,10 @@

Distribution of Answers

Details about Anonymous Users

-
+

{{userStats.anonymous.users}} have participated.

{% if userStats.anonymous.top5 %} -

Top 5 users

+

Top 5 users

@@ -297,7 +302,7 @@

Top 5 users

{% endif %}
-
+
@@ -362,10 +367,10 @@

Top 5 users

Details about Authenticated Users

-
+

{{userStats.authenticated.users}} have participated.

{% if userStats.authenticated.top5 %} -

Top 5 users

+

Top 5 users

@@ -384,7 +389,7 @@

Top 5 users

{% endif %}
-
+
+ {% if userStats.geo %}
@@ -356,6 +357,7 @@

Top 5 users

})();
+ {% endif %}
diff --git a/pybossa/view/applications.py b/pybossa/view/applications.py index 521f6eb0ad..82452b3670 100644 --- a/pybossa/view/applications.py +++ b/pybossa/view/applications.py @@ -15,7 +15,7 @@ from StringIO import StringIO import requests -from flask import Blueprint, request, url_for, flash, redirect, abort, Response +from flask import Blueprint, request, url_for, flash, redirect, abort, Response, current_app from flask import render_template, make_response from flaskext.wtf import Form, IntegerField, TextField, BooleanField, \ SelectField, validators, HiddenInput, TextAreaField @@ -794,10 +794,12 @@ def show_stats(short_name): """Returns App Stats""" app = db.session.query(model.App).filter_by(short_name=short_name).first() title = "Application: %s · Statistics" % app.name - dates_stats, hours_stats, users_stats = stats.get_stats(app.id) + dates_stats, hours_stats, users_stats = stats.get_stats(app.id, + current_app.config['GEO']) anon_pct_taskruns = int((users_stats['n_anon'] * 100) / (users_stats['n_anon'] + users_stats['n_auth'])) userStats = dict( + geo=current_app.config['GEO'], anonymous=dict( users=len(users_stats['anon']['values']), taskruns=users_stats['n_anon'], diff --git a/pybossa/web.py b/pybossa/web.py index 6af0662a17..b38b635b46 100644 --- a/pybossa/web.py +++ b/pybossa/web.py @@ -15,6 +15,7 @@ import logging import json +import os from flask import Response, request, g, render_template,\ abort, flash, redirect, session, url_for @@ -78,6 +79,13 @@ print inst print "Google singin disabled" +# Check if app stats page can generate the map +if not os.path.exists('dat/GeoLiteCity.dat'): + app.config['GEO'] = False + print("GeoLiteCity.dat file not found") + print("App page stats web map disabled") +else: + app.config['GEO'] = True def url_for_other_page(page): diff --git a/requirements.txt b/requirements.txt index a42aece0b3..00eceaedf8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,4 @@ markdown raven blinker flask-cache +pygeoip From ea9da05de78bd07182b9c6f6b389f096c3be9bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lombra=C3=B1a=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2013 12:53:12 +0100 Subject: [PATCH 20/28] Docs for enabling GeoLite Database for statistics maps. --- .gitignore | 1 + doc/customizing.rst | 25 +++++++++++++++++++++++++ doc/install.rst | 10 ++++++++-- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index bf12fd1747..f2a84dbaa8 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ pybossa/templates/custom/* _ga.html _gcs.html _gcs_form.html +dat/GeoLiteCity.dat diff --git a/doc/customizing.rst b/doc/customizing.rst index 6696d99bc7..2a6332de2b 100644 --- a/doc/customizing.rst +++ b/doc/customizing.rst @@ -319,4 +319,29 @@ And the **_gcs_form.html** will be like this:: After these steps, your site will be indexed by Google and Google Custom Search will be working, providing for your users a search tool. +Adding web maps for application statistics +========================================== +PyBossa creates for each application a statistics page, where the creators of +the application and the volunteers can check the top 5 anonymous and +authenticated users, an estimation of time about when all the tasks will be +completed, etc. + +One interesting feature of the statistics page is that it can generate a web +map showing the location of the anonymous volunteers that have been +participating in the application. By default the maps are disabled, because you +will need to download the GeoLiteCity DAT file database that will be use for +generating the maps. + +GeoLite_ is a free geolocatication database from MaxMind that they release +under a `Creative Commons Attribution-ShareAlike 3.0 Uported License`_. You can +download the required file: GeoLite City from this page_. Once you have +downloaded the file, all you have to do is to uncompress it and place it in the +folder **/dat** of the pybossa root folder. + +After copying the file, all you have to do to start creating the maps is to +restart the server. + +.. _GeoLite: http://dev.maxmind.com/geoip/geolite +.. _`Creative Commons Attribution-ShareAlike 3.0 Uported License`: http://creativecommons.org/licenses/by-sa/3.0/ +.. _page: http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz diff --git a/doc/install.rst b/doc/install.rst index 1b8db13ea9..752fad26a1 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -214,8 +214,8 @@ second one will perform the migration. Enabling a Cache ================ -PyBossa comes with a Cache system (based on `flask-cache `_) that it is -disabled by default. If you want to start caching some pages of the PyBossa server, you +PyBossa comes with a Cache system (based on `flask-cache `_) +that it is disabled by default. If you want to start caching some pages of the PyBossa server, you only have to modify your settings and change the following value from:: CACHE_TYPE = 'null' @@ -226,3 +226,9 @@ to:: The cache also supports other configurations, so please, check the official documentation of `flask-cache `_. + +.. note:: + **Important**: We highly recommend you to enable the cache, as it will boost + the performance of the server caching SQL queries as well as page views. If + you have lots of applications with hundreds of tasks, you should enable it. + From c67a88fd09afbd79c347a2676bf71a6bbc944fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lombra=C3=B1a=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2013 14:49:33 +0100 Subject: [PATCH 21/28] Unit Tests for the stats page --- test/base.py | 2 +- test/test_stats.py | 137 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 test/test_stats.py diff --git a/test/base.py b/test/base.py index 030085a382..54a441c6a2 100644 --- a/test/base.py +++ b/test/base.py @@ -149,7 +149,7 @@ def create_app(cls,info): @classmethod def create_task_and_run(cls,task_info, task_run_info, app, user, order): - task = model.Task(app_id = 1, state = '0', info = task_info) + task = model.Task(app_id = 1, state = '0', info = task_info, n_answers=10) task.app = app # Taskruns will be assigned randomly to a signed user or an anonymous one if random.randint(0,1) == 1: diff --git a/test/test_stats.py b/test/test_stats.py new file mode 100644 index 0000000000..11f346cbc1 --- /dev/null +++ b/test/test_stats.py @@ -0,0 +1,137 @@ +import datetime +import time +from base import web, model, db, Fixtures +import pybossa.stats as stats + + +class TestAdmin: + def setUp(self): + self.app = web.app + model.rebuild_db() + Fixtures.create() + + def tearDown(self): + db.session.remove() + + @classmethod + def teardown_class(cls): + model.rebuild_db() + + # Tests + # Fixtures will create 10 tasks and will need 10 answers per task, so + # the app will be completed when 100 tasks have been submitted + # Only 10 task_runs are saved in the DB + + def test_00_avg_n_tasks(self): + """Test STATS avg and n of tasks method works""" + with self.app.test_request_context('/'): + avg, n_tasks = stats.get_avg_n_tasks(1) + err_msg = "The average number of answer per task is wrong" + assert avg == 10, err_msg + err_msg = "The n of tasks is wrong" + assert n_tasks == 10, err_msg + + def test_01_stats_dates(self): + """Test STATS dates method works""" + today = unicode(datetime.date.today()) + with self.app.test_request_context('/'): + dates, dates_n_tasks, dates_anon, dates_auth = stats.stats_dates(1) + err_msg = "There should be 10 answers today" + assert dates[today] == 10, err_msg + err_msg = "There should be 100 answers per day" + assert dates_n_tasks[today] == 100, err_msg + err_msg = "The SUM of answers from anon and auth users should be 10" + assert (dates_anon[today] + dates_auth[today]) == 10, err_msg + + def test_02_stats_hours(self): + """Test STATS hours method works""" + hour = unicode(datetime.datetime.today().strftime('%H')) + with self.app.test_request_context('/'): + hours, hours_anon, hours_auth, max_hours,\ + max_hours_anon, max_hours_auth = stats.stats_hours(1) + for i in range(0, 24): + # There should be only 10 answers at current hour + if str(i) == hour: + assert hours[str(i)] == 10, "There should be 10 answers" + else: + assert hours[str(i)] == 0, "There should be 0 answers" + + if str(i) == hour: + tmp = (hours_anon[hour] + hours_auth[hour]) + assert tmp == 10, "There should be 10 answers" + else: + tmp = (hours_anon[str(i)] + hours_auth[str(i)]) + assert tmp == 0, "There should be 0 answers" + err_msg = "It should be 10, as all answer are done in the same hour" + assert max_hours == 10, err_msg + assert (max_hours_anon + max_hours_auth) == 10, err_msg + + def test_03_stats(self): + """Test STATS stats method works""" + today = unicode(datetime.date.today()) + hour = int(datetime.datetime.today().strftime('%H')) + date_ms = time.mktime(time.strptime(today, "%Y-%m-%d")) * 1000 + anon = 0 + auth = 0 + with self.app.test_request_context('/'): + dates_stats, hours_stats, user_stats = stats.get_stats(1) + for item in dates_stats: + if item['label'] == 'Anon + Auth': + assert item['values'][0][0] == date_ms, item['values'][0][0] + assert item['values'][0][1] == 10, "There should be 10 answers" + if item['label'] == 'Anonymous': + assert item['values'][0][0] == date_ms, item['values'][0][0] + anon = item['values'][0][1] + if item['label'] == 'Authenticated': + assert item['values'][0][0] == date_ms, item['values'][0][0] + auth = item['values'][0][1] + if item['label'] == 'Total': + assert item['values'][0][0] == date_ms, item['values'][0][0] + assert item['values'][0][1] == 10, "There should be 10 answers" + if item['label'] == 'Expected Answers': + assert item['values'][0][0] == date_ms, item['values'][0][0] + for i in item['values']: + assert i[1] == 100, "Each date should have 100 answers" + assert item['values'][0][1] == 100, "There should be 10 answers" + if item['label'] == 'Estimation': + assert item['values'][0][0] == date_ms, item['values'][0][0] + v = 10 + for i in item['values']: + assert i[1] == v, "Each date should have 10 extra answers" + v = v + 10 + assert auth + anon == 10, "date stats sum of auth and anon should be 10" + + max_hours = 0 + for item in hours_stats: + if item['label'] == 'Anon + Auth': + max_hours = item['max'] + assert item['max'] == 10, "Max hours value should be 10" + for i in item['values']: + if i[0] == hour: + assert i[1] == 10, "There should be 10 answers" + assert i[2] == 5, "The size of the bubble should be 5" + else: + assert i[1] == 0, "There should be 0 answers" + assert i[2] == 0, "The size of the buggle should be 0" + if item['label'] == 'Anonymous': + anon = item['max'] + for i in item['values']: + if i[0] == hour: + assert i[1] == anon, "There should be anon answers" + assert i[2] == (anon * 5) / max_hours, "The size of the bubble should be 5" + else: + assert i[1] == 0, "There should be 0 answers" + assert i[2] == 0, "The size of the buggle should be 0" + if item['label'] == 'Authenticated': + auth = item['max'] + for i in item['values']: + if i[0] == hour: + assert i[1] == auth, "There should be anon answers" + assert i[2] == (auth * 5) / max_hours, "The size of the bubble should be 5" + else: + assert i[1] == 0, "There should be 0 answers" + assert i[2] == 0, "The size of the buggle should be 0" + assert auth + anon == 10, "date stats sum of auth and anon should be 10" + + err_msg = "date stats sum of auth and anon should be 10" + assert user_stats['n_anon'] + user_stats['n_auth'], err_msg From 688509203b2d1e07596857dd175ba9365a9c8d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lombra=C3=B1a=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2013 15:11:41 +0100 Subject: [PATCH 22/28] Show a warning for apps without tasks. Closes #359 --- pybossa/templates/applications/non_stats.html | 22 +++++++ pybossa/templates/applications/stats.html | 27 +++----- pybossa/view/applications.py | 61 ++++++++++--------- 3 files changed, 63 insertions(+), 47 deletions(-) create mode 100644 pybossa/templates/applications/non_stats.html diff --git a/pybossa/templates/applications/non_stats.html b/pybossa/templates/applications/non_stats.html new file mode 100644 index 0000000000..30788afdbe --- /dev/null +++ b/pybossa/templates/applications/non_stats.html @@ -0,0 +1,22 @@ +{% extends "/base.html" %} +{% set active_page = "applications" %} +{% set active_app = "all" %} +{% import "applications/_helpers.html" as helper %} + +{% block content %} +
+
+ {{helper.render_app_local_nav(app, 'stats', current_user)}} +
+
+

+ {% if app.info.thumbnail %} + + {% else %} + + {% endif %} + {{ app.name }}: Statistics

+ Ooops Sorry, the application does not have Tasks to process and show some statistics +
+
+{% endblock %} diff --git a/pybossa/templates/applications/stats.html b/pybossa/templates/applications/stats.html index dff8a0f1c2..63c55e27cb 100644 --- a/pybossa/templates/applications/stats.html +++ b/pybossa/templates/applications/stats.html @@ -25,24 +25,14 @@ {{helper.render_app_local_nav(app, 'stats', current_user)}}
-
-
- {% if app.info.thumbnail %} -
- -
-
-

{{app.name}}: Statistics

-
- {% else %} -
-

{{app.name}}: Statistics

-
- {% endif %} -
- - - +

+ {% if app.info.thumbnail %} + + {% else %} + + {% endif %} + {{ app.name }}: Statistics

+
{% if private %}
@@ -415,6 +405,5 @@

Top 5 users

-
{% endblock %} diff --git a/pybossa/view/applications.py b/pybossa/view/applications.py index 82452b3670..dde2a5b9c7 100644 --- a/pybossa/view/applications.py +++ b/pybossa/view/applications.py @@ -794,31 +794,36 @@ def show_stats(short_name): """Returns App Stats""" app = db.session.query(model.App).filter_by(short_name=short_name).first() title = "Application: %s · Statistics" % app.name - dates_stats, hours_stats, users_stats = stats.get_stats(app.id, - current_app.config['GEO']) - anon_pct_taskruns = int((users_stats['n_anon'] * 100) / - (users_stats['n_anon'] + users_stats['n_auth'])) - userStats = dict( - geo=current_app.config['GEO'], - anonymous=dict( - users=len(users_stats['anon']['values']), - taskruns=users_stats['n_anon'], - pct_taskruns=anon_pct_taskruns, - top5=users_stats['anon']['top5']), - authenticated=dict( - users=len(users_stats['auth']['values']), - taskruns=users_stats['n_auth'], - pct_taskruns=100 - anon_pct_taskruns, - top5=users_stats['auth']['top5'])) - - tmp = dict(userStats=users_stats['users'], - userAnonStats=users_stats['anon'], - userAuthStats=users_stats['auth'], - dayStats=dates_stats, - hourStats=hours_stats) - - return render_template('/applications/stats.html', - title=title, - appStats=json.dumps(tmp), - userStats=userStats, - app=app) + if len(app.tasks)>0 and len(app.task_runs)>0: + dates_stats, hours_stats, users_stats = stats.get_stats(app.id, + current_app.config['GEO']) + anon_pct_taskruns = int((users_stats['n_anon'] * 100) / + (users_stats['n_anon'] + users_stats['n_auth'])) + userStats = dict( + geo=current_app.config['GEO'], + anonymous=dict( + users=len(users_stats['anon']['values']), + taskruns=users_stats['n_anon'], + pct_taskruns=anon_pct_taskruns, + top5=users_stats['anon']['top5']), + authenticated=dict( + users=len(users_stats['auth']['values']), + taskruns=users_stats['n_auth'], + pct_taskruns=100 - anon_pct_taskruns, + top5=users_stats['auth']['top5'])) + + tmp = dict(userStats=users_stats['users'], + userAnonStats=users_stats['anon'], + userAuthStats=users_stats['auth'], + dayStats=dates_stats, + hourStats=hours_stats) + + return render_template('/applications/stats.html', + title=title, + appStats=json.dumps(tmp), + userStats=userStats, + app=app) + else: + return render_template('/applications/non_stats.html', + title=title, + app=app) From 48f22ca120f26ed608c9ec3c62776c2a7099c420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lombra=C3=B1a=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2013 15:18:21 +0100 Subject: [PATCH 23/28] Require Python 2.7 for collections. Closes #359 --- .travis.yml | 1 - doc/install.rst | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index f3b5fcc597..b0a798f2b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - - "2.6" - "2.7" env: PYBOSSA_SETTINGS='../settings_test.py' install: diff --git a/doc/install.rst b/doc/install.rst index 752fad26a1..9b7a0b8413 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -6,7 +6,7 @@ PyBossa is a python web application built using the Flask micro-framework. Pre-requisites: - * Python >= 2.6, <3.0 + * Python >= 2.7, <3.0 * A database plus Python bindings for PostgreSQL version 9.1 * pip for installing python packages (e.g. on ubuntu python-pip) From f81892841c8e4de2c53ea313f11b1948b17d9b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lombra=C3=B1a=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2013 15:23:05 +0100 Subject: [PATCH 24/28] Fix: cache stats for 24 hours. Refs #359 --- pybossa/stats.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pybossa/stats.py b/pybossa/stats.py index 99618888f6..199a493587 100644 --- a/pybossa/stats.py +++ b/pybossa/stats.py @@ -25,7 +25,8 @@ from datetime import timedelta -STATS_TIMEOUT = 50 +# Cache Stats for 24 hours +STATS_TIMEOUT = 24*60*60 @cache.memoize(timeout=STATS_TIMEOUT) From 7a79882427c59f82b5285e9921dbcc5ec011ac7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lombra=C3=B1a=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2013 17:32:33 +0100 Subject: [PATCH 25/28] Fix geolite path --- pybossa/web.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pybossa/web.py b/pybossa/web.py index b38b635b46..994f18142e 100644 --- a/pybossa/web.py +++ b/pybossa/web.py @@ -80,7 +80,8 @@ print "Google singin disabled" # Check if app stats page can generate the map -if not os.path.exists('dat/GeoLiteCity.dat'): +geolite = app.root_path + '/../dat/GeoLiteCity.dat' +if not os.path.exists(geolite): app.config['GEO'] = False print("GeoLiteCity.dat file not found") print("App page stats web map disabled") From de0219163ab244c2bae22608142af39ba01e63d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lombra=C3=B1a=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2013 17:51:50 +0100 Subject: [PATCH 26/28] Fix missing number of tasks for anon and auth users --- pybossa/stats.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/pybossa/stats.py b/pybossa/stats.py index 199a493587..afb6c6c954 100644 --- a/pybossa/stats.py +++ b/pybossa/stats.py @@ -66,24 +66,39 @@ def stats_users(app_id): anon_users = [] # Get Authenticated Users - sql = text('''SELECT DISTINCT(task_run.user_id) AS user_id FROM task_run + sql = text('''SELECT task_run.user_id AS user_id FROM task_run WHERE task_run.user_id IS NOT NULL AND task_run.user_ip IS NULL AND task_run.app_id=:app_id;''') results = db.engine.execute(sql, app_id=app_id) for row in results: auth_users.append(row.user_id) - users['n_auth'] = len(auth_users) + + sql = text('''SELECT count(distinct(task_run.user_id)) AS user_id FROM task_run + WHERE task_run.user_id IS NOT NULL AND + task_run.user_ip IS NULL AND + task_run.app_id=:app_id;''') + results = db.engine.execute(sql, app_id=app_id) + for row in results: + users['n_auth'] = row[0] # Get Anonymous Users - sql = text('''SELECT DISTINCT(task_run.user_ip) AS user_ip FROM task_run + sql = text('''SELECT task_run.user_ip AS user_ip FROM task_run WHERE task_run.user_ip IS NOT NULL AND task_run.user_id IS NULL AND task_run.app_id=:app_id;''') results = db.engine.execute(sql, app_id=app_id) for row in results: anon_users.append(row.user_ip) - users['n_anon'] = len(anon_users) + + sql = text('''SELECT COUNT(DISTINCT(task_run.user_ip)) AS user_ip FROM task_run + WHERE task_run.user_ip IS NOT NULL AND + task_run.user_id IS NULL AND + task_run.app_id=:app_id;''') + results = db.engine.execute(sql, app_id=app_id) + + for row in results: + users['n_anon'] = row[0] return users, anon_users, auth_users From a3b1123964138c5edb4eb2281c13129c8c05dfe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lombra=C3=B1a=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2013 17:53:22 +0100 Subject: [PATCH 27/28] Fix for the template (remove Geo from table) --- pybossa/templates/applications/stats.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybossa/templates/applications/stats.html b/pybossa/templates/applications/stats.html index 63c55e27cb..52fc218b61 100644 --- a/pybossa/templates/applications/stats.html +++ b/pybossa/templates/applications/stats.html @@ -228,7 +228,7 @@

Distribution of Answers

Percentage - {% for k in userStats.keys() %} + {% for k in userStats.keys() if k != 'geo' %} {{k | capitalize}} {{userStats[k].taskruns}} From b3157af7ce14ebc221a140ead685d983d435d3c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lombra=C3=B1a=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2013 18:05:56 +0100 Subject: [PATCH 28/28] Fix geolite path --- pybossa/stats.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pybossa/stats.py b/pybossa/stats.py index afb6c6c954..62e41552b1 100644 --- a/pybossa/stats.py +++ b/pybossa/stats.py @@ -12,6 +12,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with PyBOSSA. If not, see . +from flask import current_app from sqlalchemy.sql import text from pybossa.core import cache from pybossa.core import db @@ -309,8 +310,9 @@ def stats_format_users(app_id, users, anon_users, auth_users, geo=False): top5_auth = [] loc_anon = [] # Check if the GeoLiteCity.dat exists + geolite = current_app.root_path + '/../dat/GeoLiteCity.dat' if geo: - gic = pygeoip.GeoIP('dat/GeoLiteCity.dat') + gic = pygeoip.GeoIP(geolite) for u in c_anon_users.most_common(5): if geo: loc = gic.record_by_addr(u[0])