[162] Subtle hinting that graph legends can be toggled.
--- a/ckanext/ga_report/command.py
+++ b/ckanext/ga_report/command.py
@@ -23,7 +23,7 @@
import ckan.model as model
model.Session.remove()
model.Session.configure(bind=model.meta.engine)
- log = logging.getLogger('ckanext.ga-report')
+ log = logging.getLogger('ckanext.ga_report')
import ga_model
ga_model.init_tables()
--- a/ckanext/ga_report/controller.py
+++ b/ckanext/ga_report/controller.py
@@ -1,6 +1,7 @@
import re
import csv
import sys
+import json
import logging
import operator
import collections
@@ -21,6 +22,10 @@
d = strptime(strdate, '%Y-%m')
return '%s %s' % (calendar.month_name[d.tm_mon], d.tm_year)
+def _get_unix_epoch(strdate):
+ from time import strptime,mktime
+ d = strptime(strdate, '%Y-%m')
+ return int(mktime(d))
def _month_details(cls, stat_key=None):
'''
@@ -107,11 +112,26 @@
return key, val
+ # Query historic values for sparkline rendering
+ sparkline_query = model.Session.query(GA_Stat)\
+ .filter(GA_Stat.stat_name=='Totals')\
+ .order_by(GA_Stat.period_name)
+ sparkline_data = {}
+ for x in sparkline_query:
+ sparkline_data[x.key] = sparkline_data.get(x.key,[])
+ key, val = clean_key(x.key,float(x.value))
+ tooltip = '%s: %s' % (_get_month_name(x.period_name), val)
+ sparkline_data[x.key].append( (tooltip,x.value) )
+ # Trim the latest month, as it looks like a huge dropoff
+ for key in sparkline_data:
+ sparkline_data[key] = sparkline_data[key][:-1]
+
c.global_totals = []
if c.month:
for e in entries:
key, val = clean_key(e.key, e.value)
- c.global_totals.append((key, val))
+ sparkline = sparkline_data[e.key]
+ c.global_totals.append((key, val, sparkline))
else:
d = collections.defaultdict(list)
for e in entries:
@@ -121,10 +141,18 @@
v = sum(v)
else:
v = float(sum(v))/float(len(v))
+ sparkline = sparkline_data[k]
key, val = clean_key(k,v)
- c.global_totals.append((key, val))
- c.global_totals = sorted(c.global_totals, key=operator.itemgetter(0))
+ c.global_totals.append((key, val, sparkline))
+ # Sort the global totals into a more pleasant order
+ def sort_func(x):
+ key = x[0]
+ total_order = ['Total page views','Total visits','Pages per visit']
+ if key in total_order:
+ return total_order.index(key)
+ return 999
+ c.global_totals = sorted(c.global_totals, key=sort_func)
keys = {
'Browser versions': 'browser_versions',
@@ -161,12 +189,13 @@
for k, v in keys.iteritems():
q = model.Session.query(GA_Stat).\
- filter(GA_Stat.stat_name==k)
+ filter(GA_Stat.stat_name==k).\
+ order_by(GA_Stat.period_name)
+ # Buffer the tabular data
if c.month:
entries = []
q = q.filter(GA_Stat.period_name==c.month).\
order_by('ga_stat.value::int desc')
-
d = collections.defaultdict(int)
for e in q.all():
d[e.key] += int(e.value)
@@ -175,10 +204,27 @@
entries.append((key,val,))
entries = sorted(entries, key=operator.itemgetter(1), reverse=True)
+ # Run a query on all months to gather graph data
+ graph_query = model.Session.query(GA_Stat).\
+ filter(GA_Stat.stat_name==k).\
+ order_by(GA_Stat.period_name)
+ graph_dict = {}
+ for stat in graph_query:
+ graph_dict[ stat.key ] = graph_dict.get(stat.key,{
+ 'name':stat.key,
+ 'data': []
+ })
+ graph_dict[ stat.key ]['data'].append({
+ 'x':_get_unix_epoch(stat.period_name),
+ 'y':float(stat.value)
+ })
+ graph = [ graph_dict[x[0]] for x in entries ]
+ setattr(c, v+'_graph', json.dumps( _to_rickshaw(graph,percentageMode=True) ))
+
# Get the total for each set of values and then set the value as
# a percentage of the total
if k == 'Social sources':
- total = sum([x for n,x in c.global_totals if n == 'Total visits'])
+ total = sum([x for n,x,graph in c.global_totals if n == 'Total visits'])
else:
total = sum([num for _,num in entries])
setattr(c, v, [(k,_percent(v,total)) for k,v in entries ])
@@ -203,7 +249,9 @@
writer = csv.writer(response)
writer.writerow(["Publisher Title", "Publisher Name", "Views", "Visits", "Period Name"])
- for publisher,view,visit in _get_top_publishers(None):
+ top_publishers, top_publishers_graph = _get_top_publishers(None)
+
+ for publisher,view,visit in top_publishers:
writer.writerow([publisher.title.encode('utf-8'),
publisher.name.encode('utf-8'),
view,
@@ -252,7 +300,9 @@
if c.month:
c.month_desc = ''.join([m[1] for m in c.months if m[0]==c.month])
- c.top_publishers = _get_top_publishers()
+ c.top_publishers, graph_data = _get_top_publishers()
+ c.top_publishers_graph = json.dumps( _to_rickshaw(graph_data) )
+
return render('ga_report/publisher/index.html')
def _get_packages(self, publisher=None, count=-1):
@@ -284,8 +334,9 @@
filter(GA_Stat.key==package.name)
if month != 'All': # Fetch everything unless the month is specific
dls = dls.filter(GA_Stat.period_name==month)
-
- downloads = sum(int(d.value) for d in dls.all())
+ downloads = 0
+ for x in dls:
+ downloads += int(x.value)
else:
downloads = 'No data'
top_packages.append((package, entry.pageviews, entry.visits, downloads))
@@ -336,7 +387,81 @@
c.top_packages = self._get_packages(c.publisher, 20)
+ # Graph query
+ top_package_names = [ x[0].name for x in c.top_packages ]
+ graph_query = model.Session.query(GA_Url,model.Package)\
+ .filter(model.Package.name==GA_Url.package_id)\
+ .filter(GA_Url.url.like('/dataset/%'))\
+ .filter(GA_Url.package_id.in_(top_package_names))
+ graph_dict = {}
+ for entry,package in graph_query:
+ if not package: continue
+ if entry.period_name=='All': continue
+ graph_dict[package.name] = graph_dict.get(package.name,{
+ 'name':package.title,
+ 'data':[]
+ })
+ graph_dict[package.name]['data'].append({
+ 'x':_get_unix_epoch(entry.period_name),
+ 'y':int(entry.pageviews),
+ })
+ graph = [ graph_dict[x] for x in top_package_names ]
+
+ c.graph_data = json.dumps( _to_rickshaw(graph) )
+
return render('ga_report/publisher/read.html')
+
+def _to_rickshaw(data, percentageMode=False):
+ if data==[]:
+ return data
+ # Create a consistent x-axis between all series
+ num_points = [ len(series['data']) for series in data ]
+ ideal_index = num_points.index( max(num_points) )
+ x_axis = []
+ for series in data:
+ for point in series['data']:
+ x_axis.append(point['x'])
+ x_axis = sorted( list( set(x_axis) ) )
+ # Zero pad any missing values
+ for series in data:
+ xs = [ point['x'] for point in series['data'] ]
+ for x in set(x_axis).difference(set(xs)):
+ series['data'].append( {'x':x, 'y':0} )
+ if percentageMode:
+ def get_totals(series_list):
+ totals = {}
+ for series in series_list:
+ for point in series['data']:
+ totals[point['x']] = totals.get(point['x'],0) + point['y']
+ return totals
+ # Transform data into percentage stacks
+ totals = get_totals(data)
+ # Roll insignificant series into a catch-all
+ THRESHOLD = 0.01
+ raw_data = data
+ data = []
+ for series in raw_data:
+ for point in series['data']:
+ fraction = float(point['y']) / totals[point['x']]
+ if not (series in data) and fraction>THRESHOLD:
+ data.append(series)
+ # Overwrite data with a set of intereting series
+ others = [ x for x in raw_data if not (x in data) ]
+ data.append({
+ 'name':'Other',
+ 'data': [ {'x':x,'y':y} for x,y in get_totals(others).items() ]
+ })
+ # Turn each point into a percentage
+ for series in data:
+ for point in series['data']:
+ point['y'] = (point['y']*100) / totals[point['x']]
+ # Sort the points
+ for series in data:
+ series['data'] = sorted( series['data'], key=lambda x:x['x'] )
+ # Strip the latest month's incomplete analytics
+ series['data'] = series['data'][:-1]
+ return data
+
def _get_top_publishers(limit=20):
'''
@@ -359,11 +484,39 @@
top_publishers = []
res = connection.execute(q, month)
+ department_ids = []
for row in res:
g = model.Group.get(row[0])
if g:
+ department_ids.append(row[0])
top_publishers.append((g, row[1], row[2]))
- return top_publishers
+
+ graph = []
+ if limit is not None:
+ # Query for a history graph of these publishers
+ q = model.Session.query(
+ GA_Url.department_id,
+ GA_Url.period_name,
+ func.sum(cast(GA_Url.pageviews,sqlalchemy.types.INT)))\
+ .filter( GA_Url.department_id.in_(department_ids) )\
+ .filter( GA_Url.period_name!='All' )\
+ .filter( GA_Url.url.like('/dataset/%') )\
+ .filter( GA_Url.package_id!='' )\
+ .group_by( GA_Url.department_id, GA_Url.period_name )
+ graph_dict = {}
+ for dept_id,period_name,views in q:
+ graph_dict[dept_id] = graph_dict.get( dept_id, {
+ 'name' : model.Group.get(dept_id).title,
+ 'data' : []
+ })
+ graph_dict[dept_id]['data'].append({
+ 'x': _get_unix_epoch(period_name),
+ 'y': views
+ })
+ # Sort dict into ordered list
+ for id in department_ids:
+ graph.append( graph_dict[id] )
+ return top_publishers, graph
def _get_publishers():
--- a/ckanext/ga_report/download_analytics.py
+++ b/ckanext/ga_report/download_analytics.py
@@ -32,6 +32,11 @@
first_of_this_month = datetime.datetime(date.year, date.month, 1)
_, last_day_of_month = calendar.monthrange(int(date.year), int(date.month))
last_of_this_month = datetime.datetime(date.year, date.month, last_day_of_month)
+ # if this is the latest month, note that it is only up until today
+ now = datetime.datetime.now()
+ if now.year == date.year and now.month == date.month:
+ last_day_of_month = now.day
+ last_of_this_month = now
periods = ((date.strftime(FORMAT_MONTH),
last_day_of_month,
first_of_this_month, last_of_this_month),)
@@ -126,7 +131,7 @@
# Make sure the All records are correct.
ga_model.post_update_url_stats()
- log.info('Aggregating datasets by publisher')
+ log.info('Associating datasets with their publisher')
ga_model.update_publisher_stats(period_name) # about 30 seconds.
@@ -298,7 +303,7 @@
def _download_stats(self, start_date, end_date, period_name, period_complete_day):
- """ Fetches stats about language and country """
+ """ Fetches stats about data downloads """
import ckan.model as model
data = {}
@@ -320,7 +325,14 @@
return
def process_result_data(result_data, cached=False):
+ progress_total = len(result_data)
+ progress_count = 0
+ resources_not_matched = []
for result in result_data:
+ progress_count += 1
+ if progress_count % 100 == 0:
+ log.debug('.. %d/%d done so far', progress_count, progress_total)
+
url = result[0].strip()
# Get package id associated with the resource that has this URL.
@@ -334,9 +346,13 @@
if package_name:
data[package_name] = data.get(package_name, 0) + int(result[1])
else:
- log.warning(u"Could not find resource for URL: {url}".format(url=url))
+ resources_not_matched.append(url)
continue
-
+ if resources_not_matched:
+ log.debug('Could not match %i or %i resource URLs to datasets. e.g. %r',
+ len(resources_not_matched), progress_total, resources_not_matched[:3])
+
+ log.info('Associating downloads of resource URLs with their respective datasets')
process_result_data(results.get('rows'))
results = self.service.data().ga().get(
@@ -348,6 +364,7 @@
dimensions="ga:eventLabel",
max_results=10000,
end_date=end_date).execute()
+ log.info('Associating downloads of cache resource URLs with their respective datasets')
process_result_data(results.get('rows'), cached=False)
self._filter_out_long_tail(data, MIN_DOWNLOADS)
--- a/ckanext/ga_report/ga_model.py
+++ b/ckanext/ga_report/ga_model.py
@@ -161,20 +161,20 @@
def pre_update_url_stats(period_name):
- log.debug("Deleting '%s' records" % period_name)
- model.Session.query(GA_Url).\
- filter(GA_Url.period_name==period_name).delete()
-
- count = model.Session.query(GA_Url).\
- filter(GA_Url.period_name == 'All').count()
- log.debug("Deleting %d 'All' records" % count)
- count = model.Session.query(GA_Url).\
- filter(GA_Url.period_name == 'All').delete()
- log.debug("Deleted %d 'All' records" % count)
+ q = model.Session.query(GA_Url).\
+ filter(GA_Url.period_name==period_name)
+ log.debug("Deleting %d '%s' records" % (q.count(), period_name))
+ q.delete()
+
+ q = model.Session.query(GA_Url).\
+ filter(GA_Url.period_name == 'All')
+ log.debug("Deleting %d 'All' records..." % q.count())
+ q.delete()
model.Session.flush()
model.Session.commit()
model.repo.commit_and_remove()
+ log.debug('...done')
def post_update_url_stats():
@@ -185,6 +185,7 @@
record regardless of whether the URL has an entry for
the month being currently processed.
"""
+ log.debug('Post-processing "All" records...')
query = """select url, pageviews::int, visits::int
from ga_url
where url not in (select url from ga_url where period_name ='All')"""
@@ -197,7 +198,13 @@
views[row[0]] = views.get(row[0], 0) + row[1]
visits[row[0]] = visits.get(row[0], 0) + row[2]
+ progress_total = len(views.keys())
+ progress_count = 0
for key in views.keys():
+ progress_count += 1
+ if progress_count % 100 == 0:
+ log.debug('.. %d/%d done so far', progress_count, progress_total)
+
package, publisher = _get_package_and_publisher(key)
values = {'id': make_uuid(),
@@ -207,10 +214,11 @@
'pageviews': views[key],
'visits': visits[key],
'department_id': publisher,
- 'package_id': publisher
+ 'package_id': package
}
model.Session.add(GA_Url(**values))
model.Session.commit()
+ log.debug('..done')
def update_url_stats(period_name, period_complete_day, url_data):
@@ -219,9 +227,14 @@
stores them in GA_Url under the period and recalculates the totals for
the 'All' period.
'''
+ progress_total = len(url_data)
+ progress_count = 0
for url, views, visits in url_data:
+ progress_count += 1
+ if progress_count % 100 == 0:
+ log.debug('.. %d/%d done so far', progress_count, progress_total)
+
package, publisher = _get_package_and_publisher(url)
-
item = model.Session.query(GA_Url).\
filter(GA_Url.period_name==period_name).\
--- a/ckanext/ga_report/helpers.py
+++ b/ckanext/ga_report/helpers.py
@@ -106,6 +106,10 @@
for entry in entries:
if len(datasets) < count:
p = model.Package.get(entry.url[len('/dataset/'):])
+ if not p:
+ _log.warning("Could not find Package for {url}".format(url=entry.url))
+ continue
+
if not p in datasets:
datasets[p] = {'views':0, 'visits': 0}
datasets[p]['views'] = datasets[p]['views'] + int(entry.pageviews)
--- /dev/null
+++ b/ckanext/ga_report/public/css/ga_report.css
@@ -1,1 +1,59 @@
+.table-condensed td.sparkline-cell {
+ padding: 1px 0 0 0;
+ width: 108px;
+ text-align: center;
+}
+.rickshaw_chart_container {
+ position: relative;
+ height: 350px;
+ margin: 0 auto 20px auto;
+}
+.rickshaw_chart {
+ position: absolute;
+ left: 40px;
+ width: 500px;
+ top: 0;
+ bottom: 0;
+}
+.rickshaw_legend {
+ background: transparent;
+ width: 100%;
+ padding-top: 4px;
+}
+.rickshaw_y_axis {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 40px;
+}
+.rickshaw_legend .label {
+ background: transparent !important;
+ color: #000000 !important;
+ font-weight: normal !important;
+}
+.rickshaw_legend .instructions {
+ color: #000;
+ margin-bottom: 6px;
+}
+.rickshaw_legend .line .action {
+ display: none;
+}
+.rickshaw_legend .line .swatch {
+ display: block;
+ float: left;
+}
+.rickshaw_legend .line .label {
+ display: block;
+ white-space: normal;
+ float: left;
+ width: 200px;
+}
+.rickshaw_legend .line .label:hover {
+ text-decoration: underline;
+}
+
+.ga-reports-table .td-numeric {
+ text-align: center;
+}
+
--- /dev/null
+++ b/ckanext/ga_report/public/scripts/ckanext_ga_reports.js
@@ -1,1 +1,134 @@
+var CKAN = CKAN || {};
+CKAN.GA_Reports = {};
+CKAN.GA_Reports.render_rickshaw = function( css_name, data, mode, colorscheme ) {
+ var graphLegends = $('#graph-legend-container');
+
+ if (!Modernizr.svg) {
+ $("#chart_"+css_name)
+ .html( '<div class="alert">Your browser does not support vector graphics. No graphs can be rendered.</div>')
+ .closest('.rickshaw_chart_container').css('height',50);
+ var myLegend = $('<div id="legend_'+css_name+'"/>')
+ .html('(Graph cannot be rendered)')
+ .appendTo(graphLegends);
+ return;
+ }
+ var myLegend = $('<div id="legend_'+css_name+'"/>').appendTo(graphLegends);
+
+ var palette = new Rickshaw.Color.Palette( { scheme: colorscheme } );
+ $.each(data, function(i, object) {
+ object['color'] = palette.color();
+ });
+ // Rickshaw renders the legend in reverse order...
+ data.reverse();
+
+ var graphElement = document.querySelector("#chart_"+css_name);
+
+ var graph = new Rickshaw.Graph( {
+ element: document.querySelector("#chart_"+css_name),
+ renderer: mode,
+ series: data ,
+ height: 328
+ });
+ var x_axis = new Rickshaw.Graph.Axis.Time( { graph: graph } );
+ var y_axis = new Rickshaw.Graph.Axis.Y( {
+ graph: graph,
+ orientation: 'left',
+ tickFormat: Rickshaw.Fixtures.Number.formatKMBT,
+ element: document.getElementById('y_axis_'+css_name)
+ } );
+ var legend = new Rickshaw.Graph.Legend( {
+ element: document.querySelector('#legend_'+css_name),
+ graph: graph
+ } );
+ var shelving = new Rickshaw.Graph.Behavior.Series.Toggle( {
+ graph: graph,
+ legend: legend
+ } );
+ myLegend.prepend('<div class="instructions">Click on a series below to isolate its graph:</div>');
+ graph.render();
+};
+
+CKAN.GA_Reports.bind_sparklines = function() {
+ /*
+ * Bind to the 'totals' tab being on screen, when the
+ * Sparkline graphs should be drawn.
+ * Note that they cannot be drawn sooner.
+ */
+ $('a[href="#totals"]').on(
+ 'shown',
+ function() {
+ var sparkOptions = {
+ enableTagOptions: true,
+ type: 'line',
+ width: 100,
+ height: 26,
+ chartRangeMin: 0,
+ spotColor: '',
+ maxSpotColor: '',
+ minSpotColor: '',
+ highlightSpotColor: '000000',
+ lineColor: '3F8E6D',
+ fillColor: 'B7E66B'
+ };
+ $('.sparkline').sparkline('html',sparkOptions);
+ }
+ );
+};
+
+CKAN.GA_Reports.bind_sidebar = function() {
+ /*
+ * Bind to changes in the tab behaviour:
+ * Show the correct rickshaw graph in the sidebar.
+ * Not to be called before all graphs load.
+ */
+ $('a[data-toggle="hashchange"]').on(
+ 'shown',
+ function(e) {
+ var href = $(e.target).attr('href');
+ var pane = $(href);
+ if (!pane.length) { console.err('bad href',href); return; }
+ var legend_name = "none";
+ var graph = pane.find('.rickshaw_chart');
+ if (graph.length) {
+ legend_name = graph.attr('id').replace('chart_','');
+ }
+ legend_name = '#legend_'+legend_name;
+ $('#graph-legend-container > *').hide();
+ $('#graph-legend-container .instructions').show();
+ $(legend_name).show();
+ }
+ );
+};
+
+CKAN.GA_Reports.bind_month_selector = function() {
+ var handler = function(e) {
+ var target = $(e.delegateTarget);
+ var form = target.closest('form');
+ var url = form.attr('action')+'?month='+target.val()+window.location.hash;
+ window.location = url;
+ };
+ var selectors = $('select[name="month"]');
+ assert(selectors.length>0);
+ selectors.bind('change', handler);
+};
+
+/*
+ * Custom bootstrap plugin for handling data-toggle="hashchange".
+ * Behaves like data-toggle="tab" but I respond to the hashchange.
+ * Page state is memo-ized in the URL this way. Why doesn't Bootstrap do this?
+ */
+$(function() {
+ var mapping = {};
+ $('a[data-toggle="hashchange"]').each(
+ function(i,link) {
+ link = $(link);
+ mapping[link.attr('href')] = link;
+ }
+ );
+ $(window).hashchange(function() {
+ var link = mapping[window.location.hash];
+ if (link) { link.tab('show'); }
+ });
+});
+
--- /dev/null
+++ b/ckanext/ga_report/public/scripts/rickshaw_ie7_shim.js
@@ -1,1 +1,109 @@
+/*
+ * Collection of shims to allow d3 and Rickshaw to load, error-free
+ * (but ultimately unusable) on Internet Explorer 7. The browser's
+ * API lacks several crucial functions which these libraries depend
+ * upon to load; we try to hide these errors from the user.
+ *
+ * With thanks to Array functions from:
+ * http://stackoverflow.com/questions/2790001/fixing-javascript-array-functions-in-internet-explorer-indexof-foreach-etc
+ *
+ * Use (Modernizr.svg==true) to detect whether it's okay to draw a graph.
+ */
+'use strict';
+window.Element = window.Element || {'prototype': {}};
+window.CSSStyleDeclaration = window.CSSStyleDeclaration || {'prototype':{}};
+
+// Add ECMA262-5 method binding if not supported natively
+//
+if (!('bind' in Function.prototype)) {
+ Function.prototype.bind= function(owner) {
+ var that= this;
+ if (arguments.length<=1) {
+ return function() {
+ return that.apply(owner, arguments);
+ };
+ } else {
+ var args= Array.prototype.slice.call(arguments, 1);
+ return function() {
+ return that.apply(owner, arguments.length===0? args : args.concat(Array.prototype.slice.call(arguments)));
+ };
+ }
+ };
+}
+
+// Add ECMA262-5 string trim if not supported natively
+//
+if (!('trim' in String.prototype)) {
+ String.prototype.trim= function() {
+ return this.replace(/^\s+/, '').replace(/\s+$/, '');
+ };
+}
+
+// Add ECMA262-5 Array methods if not supported natively
+//
+if (!('indexOf' in Array.prototype)) {
+ Array.prototype.indexOf= function(find, i /*opt*/) {
+ if (i===undefined) i= 0;
+ if (i<0) i+= this.length;
+ if (i<0) i= 0;
+ for (var n= this.length; i<n; i++)
+ if (i in this && this[i]===find)
+ return i;
+ return -1;
+ };
+}
+if (!('lastIndexOf' in Array.prototype)) {
+ Array.prototype.lastIndexOf= function(find, i /*opt*/) {
+ if (i===undefined) i= this.length-1;
+ if (i<0) i+= this.length;
+ if (i>this.length-1) i= this.length-1;
+ for (i++; i-->0;) /* i++ because from-argument is sadly inclusive */
+ if (i in this && this[i]===find)
+ return i;
+ return -1;
+ };
+}
+if (!('forEach' in Array.prototype)) {
+ Array.prototype.forEach= function(action, that /*opt*/) {
+ for (var i= 0, n= this.length; i<n; i++)
+ if (i in this)
+ action.call(that, this[i], i, this);
+ };
+}
+if (!('map' in Array.prototype)) {
+ Array.prototype.map= function(mapper, that /*opt*/) {
+ var other= new Array(this.length);
+ for (var i= 0, n= this.length; i<n; i++)
+ if (i in this)
+ other[i]= mapper.call(that, this[i], i, this);
+ return other;
+ };
+}
+if (!('filter' in Array.prototype)) {
+ Array.prototype.filter= function(filter, that /*opt*/) {
+ var other= [], v;
+ for (var i=0, n= this.length; i<n; i++)
+ if (i in this && filter.call(that, v= this[i], i, this))
+ other.push(v);
+ return other;
+ };
+}
+if (!('every' in Array.prototype)) {
+ Array.prototype.every= function(tester, that /*opt*/) {
+ for (var i= 0, n= this.length; i<n; i++)
+ if (i in this && !tester.call(that, this[i], i, this))
+ return false;
+ return true;
+ };
+}
+if (!('some' in Array.prototype)) {
+ Array.prototype.some= function(tester, that /*opt*/) {
+ for (var i= 0, n= this.length; i<n; i++)
+ if (i in this && tester.call(that, this[i], i, this))
+ return true;
+ return false;
+ };
+}
+
+
--- /dev/null
+++ b/ckanext/ga_report/public/scripts/vendor/d3.layout.min.js
@@ -1,1 +1,1 @@
-
+(function(){function a(a){var b=a.source,d=a.target,e=c(b,d),f=[b];while(b!==e)b=b.parent,f.push(b);var g=f.length;while(d!==e)f.splice(g,0,d),d=d.parent;return f}function b(a){var b=[],c=a.parent;while(c!=null)b.push(a),a=c,c=c.parent;return b.push(a),b}function c(a,c){if(a===c)return a;var d=b(a),e=b(c),f=d.pop(),g=e.pop(),h=null;while(f===g)h=f,f=d.pop(),g=e.pop();return h}function g(a){a.fixed|=2}function h(a){a!==f&&(a.fixed&=1)}function i(){j(),f.fixed&=1,e=f=null}function j(){f.px+=d3.event.dx,f.py+=d3.event.dy,e.resume()}function k(a,b,c){var d=0,e=0;a.charge=0;if(!a.leaf){var f=a.nodes,g=f.length,h=-1,i;while(++h<g){i=f[h];if(i==null)continue;k(i,b,c),a.charge+=i.charge,d+=i.charge*i.cx,e+=i.charge*i.cy}}if(a.point){a.leaf||(a.point.x+=Math.random()-.5,a.point.y+=Math.random()-.5);var j=b*c[a.point.index];a.charge+=a.pointCharge=j,d+=j*a.point.x,e+=j*a.point.y}a.cx=d/a.charge,a.cy=e/a.charge}function l(a){return 20}function m(a){return 1}function o(a){return a.x}function p(a){return a.y}function q(a,b,c){a.y0=b,a.y=c}function t(a){var b=1,c=0,d=a[0][1],e,f=a.length;for(;b<f;++b)(e=a[b][1])>d&&(c=b,d=e);return c}function u(a){return a.reduce(v,0)}function v(a,b){return a+b[1]}function w(a,b){return x(a,Math.ceil(Math.log(b.length)/Math.LN2+1))}function x(a,b){var c=-1,d=+a[0],e=(a[1]-d)/b,f=[];while(++c<=b)f[c]=e*c+d;return f}function y(a){return[d3.min(a),d3.max(a)]}function z(a,b){return a.sort=d3.rebind(a,b.sort),a.children=d3.rebind(a,b.children),a.links=D,a.value=d3.rebind(a,b.value),a.nodes=function(b){return E=!0,(a.nodes=a)(b)},a}function A(a){return a.children}function B(a){return a.value}function C(a,b){return b.value-a.value}function D(a){return d3.merge(a.map(function(a){return(a.children||[]).map(function(b){return{source:a,target:b}})}))}function F(a,b){return a.value-b.value}function G(a,b){var c=a._pack_next;a._pack_next=b,b._pack_prev=a,b._pack_next=c,c._pack_prev=b}function H(a,b){a._pack_next=b,b._pack_prev=a}function I(a,b){var c=b.x-a.x,d=b.y-a.y,e=a.r+b.r;return e*e-c*c-d*d>.001}function J(a){function l(a){b=Math.min(a.x-a.r,b),c=Math.max(a.x+a.r,c),d=Math.min(a.y-a.r,d),e=Math.max(a.y+a.r,e)}var b=Infinity,c=-Infinity,d=Infinity,e=-Infinity,f=a.length,g,h,i,j,k;a.forEach(K),g=a[0],g.x=-g.r,g.y=0,l(g);if(f>1){h=a[1],h.x=h.r,h.y=0,l(h);if(f>2){i=a[2],O(g,h,i),l(i),G(g,i),g._pack_prev=i,G(i,h),h=g._pack_next;for(var m=3;m<f;m++){O(g,h,i=a[m]);var n=0,o=1,p=1;for(j=h._pack_next;j!==h;j=j._pack_next,o++)if(I(j,i)){n=1;break}if(n==1)for(k=g._pack_prev;k!==j._pack_prev;k=k._pack_prev,p++)if(I(k,i)){p<o&&(n=-1,j=k);break}n==0?(G(g,i),h=i,l(i)):n>0?(H(g,j),h=j,m--):(H(j,h),g=j,m--)}}}var q=(b+c)/2,r=(d+e)/2,s=0;for(var m=0;m<f;m++){var t=a[m];t.x-=q,t.y-=r,s=Math.max(s,t.r+Math.sqrt(t.x*t.x+t.y*t.y))}return a.forEach(L),s}function K(a){a._pack_next=a._pack_prev=a}function L(a){delete a._pack_next,delete a._pack_prev}function M(a){var b=a.children;b&&b.length?(b.forEach(M),a.r=J(b)):a.r=Math.sqrt(a.value)}function N(a,b,c,d){var e=a.children;a.x=b+=d*a.x,a.y=c+=d*a.y,a.r*=d;if(e){var f=-1,g=e.length;while(++f<g)N(e[f],b,c,d)}}function O(a,b,c){var d=a.r+c.r,e=b.x-a.x,f=b.y-a.y;if(d&&(e||f)){var g=b.r+c.r,h=Math.sqrt(e*e+f*f),i=Math.max(-1,Math.min(1,(d*d+h*h-g*g)/(2*d*h))),j=Math.acos(i),k=i*(d/=h),l=Math.sin(j)*d;c.x=a.x+k*e+l*f,c.y=a.y+k*f-l*e}else c.x=a.x+d,c.y=a.y}function P(a){return 1+d3.max(a,function(a){return a.y})}function Q(a){return a.reduce(function(a,b){return a+b.x},0)/a.length}function R(a){var b=a.children;return b&&b.length?R(b[0]):a}function S(a){var b=a.children,c;return b&&(c=b.length)?S(b[c-1]):a}function T(a,b){return a.parent==b.parent?1:2}function U(a){var b=a.children;return b&&b.length?b[0]:a._tree.thread}function V(a){var b=a.children,c;return b&&(c=b.length)?b[c-1]:a._tree.thread}function W(a,b){var c=a.children;if(c&&(e=c.length)){var d,e,f=-1;while(++f<e)b(d=W(c[f],b),a)>0&&(a=d)}return a}function X(a,b){return a.x-b.x}function Y(a,b){return b.x-a.x}function Z(a,b){return a.depth-b.depth}function $(a,b){function c(a,d){var e=a.children;if(e&&(i=e.length)){var f,g=null,h=-1,i;while(++h<i)f=e[h],c(f,g),g=f}b(a,d)}c(a,null)}function _(a){var b=0,c=0,d=a.children,e=d.length,f;while(--e>=0)f=d[e]._tree,f.prelim+=b,f.mod+=b,b+=f.shift+(c+=f.change)}function ba(a,b,c){a=a._tree,b=b._tree;var d=c/(b.number-a.number);a.change+=d,b.change-=d,b.shift+=c,b.prelim+=c,b.mod+=c}function bb(a,b,c){return a._tree