Milimetric has submitted this change and it was merged.

Change subject: Report session funnel weekly instead of daily
......................................................................


Report session funnel weekly instead of daily

To save space, we aggregate the existing output weekly and remove any
unique sequence of actions that represent less than 0.08% of the total
for that week (because these would be hidden anyway in the most granular
view of the visualization since they would create slices that are too
small to occupy pixels).  Going forward, we query data weekly.  Sadly,
I couldn't find an efficient way to remove the "less than < 0.08%"
outliers and so we'd have to trim them later with the squish script if
we wanted.  Hopefully moving to Hadoop will make this query perform
better.

NOTE: before merging this, make sure to run the included python script
on the existing sessions.sql output.

Bug: T147492
Change-Id: Id59e97dc650213f34207cbaacf538642815db658
---
M edit/config.yaml
M edit/sessions.sql
A scripts/aggregate-and-filter-sessions.py
D static/adhoc.html
D static/funnel-prototype.html
D static/funnel-prototype.js
D static/stacked-bar-prototype.html
D static/stacked-bar-prototype.js
D static/util.js
9 files changed, 138 insertions(+), 1,271 deletions(-)

Approvals:
  Milimetric: Verified; Looks good to me, approved



diff --git a/edit/config.yaml b/edit/config.yaml
index 7932197..8675c5d 100644
--- a/edit/config.yaml
+++ b/edit/config.yaml
@@ -11,7 +11,7 @@
 reports:
 
     sessions:
-        granularity: days
+        granularity: weeks
         funnel: true
         starts: 2015-04-01
         explode_by:
diff --git a/edit/sessions.sql b/edit/sessions.sql
index f90aeb5..130f090 100644
--- a/edit/sessions.sql
+++ b/edit/sessions.sql
@@ -1,70 +1,55 @@
- select day,
+ select date('{from_timestamp}') as day,
         actions,
         count(*) as repeated
-   from (
-      select
-         date(timestamp) as day,
-         group_concat(event_action order by timestamp, action_order.ord 
separator '-') as actions
-      from (
-         select
-            timestamp,
-            event_action,
-            clientIp,
-            event_editingSessionId
-         from Edit_11448630
-         where
-            event_editor = '{editor}' and
-            ('{wiki}' = 'all' or wiki = '{wiki}') and
-            timestamp >= '{from_timestamp}' and
-            timestamp < '{to_timestamp}'
+   from (select group_concat(event_action order by timestamp, action_order.ord 
separator '-') as actions
+           from (select timestamp,
+                        event_action,
+                        clientIp,
+                        event_editingSessionId
 
-         # Add in events using the new schema. To be removed once everything
-         # is switched over.
-         union all
+                   from Edit_13457736
+                        # NOTE: used to union all with Edit_11448630
+                        # That schema is still used apparently, but very little
 
-         select
-            timestamp,
-            event_action,
-            clientIp,
-            event_editingSessionId
-         from Edit_13457736
-         where
-            event_editor = '{editor}' and
-            ('{wiki}' = 'all' or wiki = '{wiki}') and
-            timestamp >= '{from_timestamp}' and
-            timestamp < '{to_timestamp}'
-         ) raw_events
+                  where event_editor = '{editor}'
+                    and ('{wiki}' = 'all' or wiki = '{wiki}')
+                    and timestamp >= '{from_timestamp}'
+                    and timestamp < '{to_timestamp}'
+                ) raw_events
 
-         inner join
-            (select 'init' as action, 0 as ord
-                union all
-             select 'ready' as action, 1 as ord
-                union all
-             select 'saveIntent' as action, 2 as ord
-                union all
-             select 'saveAttempt' as action, 3 as ord
-                union all
-             select 'saveFailure' as action, 4 as ord
-                union all
-             select 'saveSuccess' as action, 5 as ord
-                union all
-             select 'abort' as action, 6 as ord
-            ) action_order
-         on action = event_action
+                    inner join
+
+                (select 'init' as action, 0 as ord
+                    union all
+                 select 'ready' as action, 1 as ord
+                    union all
+                 select 'saveIntent' as action, 2 as ord
+                    union all
+                 select 'saveAttempt' as action, 3 as ord
+                    union all
+                 select 'saveFailure' as action, 4 as ord
+                    union all
+                 select 'saveSuccess' as action, 5 as ord
+                    union all
+                 select 'abort' as action, 6 as ord
+
+                ) action_order  on action = event_action
 
          # client side, the ip is set but the editingSessionId is bad
          # due to a bad library in Safari, it creates duplicates
          # server side, the ip is not set, but editingSessionId is good
-         group by if(
-           if( '{editor}' = 'wikitext', 0, 1 ),
-           concat( coalesce( clientIp, '' ), event_editingSessionId ),
-           event_editingSessionId
-        )
+          group by if(
+                    if( '{editor}' = 'wikitext', 0, 1 ),
+                    concat( coalesce( clientIp, '' ), event_editingSessionId ),
+                    event_editingSessionId
+                )
 
          # this purposefully ignores sessions that start before 
"from_timestamp",
          # as well as some other sessions that legitimately do not have an 
"init"
          # as well as sessions that have an "init" after "ready"
          having actions like 'init%'
+
    ) sessions
-  group by day, actions
+
+  group by actions
 ;
diff --git a/scripts/aggregate-and-filter-sessions.py 
b/scripts/aggregate-and-filter-sessions.py
new file mode 100644
index 0000000..82b35ae
--- /dev/null
+++ b/scripts/aggregate-and-filter-sessions.py
@@ -0,0 +1,98 @@
+"""
+Crawls a directory in the structure directory/{submetric}/{wiki}.tsv
+For each tsv file, it takes each line, parses the date and finds
+the most recent previous Sunday.  It then rolls up all other lines that
+belong to the same Sunday and outputs the results.  So it aggregates
+weekly in this manner.  The script also removes any rows in a week that
+represent less than 0.08% of the total for that week.  These would not
+be rendered in a graph because they're too small, and there are lots of
+them, like 90% of the total lines by count.
+
+Usage
+    python aggregate-and-filter-sessions.py input-directory output-directory
+"""
+import csv
+import glob
+import sys
+
+from path import path
+from collections import OrderedDict, defaultdict
+from datetime import datetime, timedelta
+
+INPUT_DIR = path(sys.argv[1]).abspath()
+OUTPUT_DIR = path(sys.argv[2]).abspath()
+DATE_FORMAT = '%Y-%m-%d'
+
+
+def getDate(dtString):
+    return datetime.strptime(dtString, DATE_FORMAT)
+
+
+def toString(dt):
+    return dt.strftime(DATE_FORMAT)
+
+
+def lastSunday(dt):
+    return dt - timedelta(days=(dt.isoweekday() % 7))
+
+
+def addWeekToOutput(output, currentWeek, weekDate):
+    totalCounts = sum(currentWeek.values())
+    output[weekDate] = {
+        k: v
+        for k, v in currentWeek.items()
+        # NOTE: this filter trims out any line which represents
+        # less than 0.08% of all actions when added up.  But this
+        # is incorrect because the actions represent a tree and can
+        # not be meaningfully filtered until that tree is first built
+        # So this is a crude approximation and will result in loss of
+        # data.  On the other hand, the current file size is much too
+        # bigly, so this is better than nothing.  This specific magic
+        # number is the same the visualization uses to filter out
+        # sunburst sections that would otherwise not receive any visual
+        # pixels when rendered.  Again, though, that number is not
+        # applicable here.
+        if v > (0.0008 * totalCounts)
+    }
+
+
+for wiki in glob.glob('{inputDir}/*/*.tsv'.format(inputDir=INPUT_DIR)):
+    output = OrderedDict()
+
+    with open(wiki) as f:
+        currentWeek = defaultdict(lambda: 0)
+        previousDateKey = None
+        dateKey = None
+        header = True
+
+        for row in csv.reader(f, delimiter='\t'):
+            if header:
+                output[row[0]] = {row[1]: row[2]}
+                header = False
+                continue
+
+            dateKey = toString(lastSunday(getDate(row[0])))
+            actionsKey = row[1]
+            actionCount = int(row[2] or 0)
+
+            if dateKey != previousDateKey:
+                addWeekToOutput(output, currentWeek, previousDateKey)
+                previousDateKey = dateKey
+                currentWeek = defaultdict(lambda: 0)
+
+            currentWeek[actionsKey] += actionCount
+
+    if len(currentWeek.keys()):
+        addWeekToOutput(output, currentWeek, previousDateKey)
+
+    outputFilePath = wiki.replace(INPUT_DIR, OUTPUT_DIR)
+    # make sure the parent directories exist
+    path(outputFilePath).dirname().makedirs_p()
+
+    with open(outputFilePath, 'w') as o:
+        writer = csv.writer(o, delimiter='\t')
+        writer.writerows((
+            [d, k, a[k]]
+            for d, a in output.items()
+            for k in sorted(a.keys())
+        ))
diff --git a/static/adhoc.html b/static/adhoc.html
deleted file mode 100644
index 2762a06..0000000
--- a/static/adhoc.html
+++ /dev/null
@@ -1,100 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset='utf-8'>
-    <title>Editing Analysis</title>
-
-    <script src='//cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js'></script>
-    <script 
src='//cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js'></script>
-    <script 
src='//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
-    <script 
src='//cdnjs.cloudflare.com/ajax/libs/rickshaw/1.5.1/rickshaw.min.js'></script>
-
-    <link rel='stylesheet' type='text/css'
-      href='//fonts.googleapis.com/css?family=Open+Sans:400,600'>
-    <link rel='stylesheet' type='text/css'
-      href='//cdnjs.cloudflare.com/ajax/libs/rickshaw/1.5.1/rickshaw.min.css'>
-
-    <style type='text/css'>
-        #timeseries { float: left; }
-        #timeseriesLegend { margin-left: 30px; }
-        #timeline { float: left; clear: left; }
-        #main { margin-top: 30px; }
-    </style>
-
-  </head>
-  <body>
-    <h2 class='header'></h2>
-    <div id='timeseries'></div>
-    <div id='timeseriesLegend'></div>
-    <div id='timeline'></div>
-
-    <hr style='clear: both; visibility: hidden;'/>
-
-    <table class='data'></table>
-
-    <script type='text/javascript' src='util.js'></script>
-    <script type='text/javascript'>
-        var color = d3.scale.category10(),
-            data = '';
-
-        if (location.hash) {
-            data = location.hash.substring(1);
-        }
-        d3.text(data, function(text) {
-            $('.header').text(data);
-            var header = d3.tsv.parseRows(text),
-                rows = header.splice(1);
-
-            // show as a table for reference
-            var table = d3.select('table.data')
-                .style('margin-top', '40px')
-                .style('clear', 'both');
-            table.append('thead')
-                .style('background-color', '#112233')
-                .style('color', '#ffffff')
-                .selectAll('tr').data(header)
-                    .enter().append('tr')
-                    .selectAll('th').data(function(d) { return d; })
-                        .enter().append('th')
-                        .style('padding', '4px')
-                        .text(function (d) { return d; });
-
-            table.append('tbody')
-                .selectAll('tr').data(rows)
-                .enter().append('tr')
-                    .selectAll('td').data(function(d) { return d; })
-                        .enter().append('td')
-                        .text(function (d, i) { return i === 0 ? d : 
d3.round(d, 2); })
-                        .style('padding', '4px')
-                        .style('background-color', function (d, i) {
-                            return i % 2 == 0 ? '#dadada' : '#ffffff';
-                        });
-
-
-            var series = header[0].slice(1).filter(function (col, i) {
-                return i === 0 || isFinite(rows[1][i+1]);
-            }).map(function (col, i) {
-                return {
-                    name: col,
-                    color: color(col),
-                    data: rows.map(function (row) {
-                        var rawDate = row[0];
-                        if (isFinite(rawDate) && rawDate.length === 8) {
-                            rawDate = rawDate.substr(0,4) + '-' + 
rawDate.substr(4,2) + '-' + rawDate.substr(6,2);
-                        }
-                        return {
-                            y: parseFloat(row[i+1]), // +1 because column 1 is 
the day
-                            x: Date.parse(rawDate) / 1000
-                        };
-                    })
-                };
-            });
-
-            createTimeseries(series, {
-                width: 900,
-                height: 500
-            });
-        });
-    </script>
-  </body>
-</html>
diff --git a/static/funnel-prototype.html b/static/funnel-prototype.html
deleted file mode 100644
index 6b58b25..0000000
--- a/static/funnel-prototype.html
+++ /dev/null
@@ -1,122 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset='utf-8'>
-    <title>Visual Editor by Day by Wiki</title>
-
-    <script src='//cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js'></script>
-    <script 
src='//cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js'></script>
-    <script 
src='//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
-    <script 
src='//cdnjs.cloudflare.com/ajax/libs/rickshaw/1.5.1/rickshaw.min.js'></script>
-
-    <link rel='stylesheet' type='text/css'
-      href='//fonts.googleapis.com/css?family=Open+Sans:400,600'>
-    <link rel='stylesheet' type='text/css'
-      href='//cdnjs.cloudflare.com/ajax/libs/rickshaw/1.5.1/rickshaw.min.css'>
-
-    <style type='text/css'>
-        body {
-          font-family: 'Open Sans', sans-serif;
-          font-size: 12px;
-          font-weight: 400;
-          background-color: #fff;
-          width: 960px;
-          height: 700px;
-          margin-top: 10px;
-        }
-
-        #main {
-          float: left;
-          width: 750px;
-        }
-
-        #sidebar {
-          float: right;
-          width: 100px;
-        }
-
-        #sequence {
-          width: 600px;
-          height: 70px;
-        }
-
-        #legend {
-          padding: 10px 0 0 3px;
-        }
-
-        #sequence text, #legend text {
-          font-weight: 600;
-          fill: #fff;
-        }
-
-        #chart {
-          position: relative;
-        }
-
-        #chart path {
-          stroke: #fff;
-        }
-
-        #explanation {
-          position: absolute;
-          top: 250px;
-          left: 340px;
-          width: 170px;
-          text-align: center;
-          color: #666;
-          z-index: -1;
-        }
-
-        #percentage {
-          font-size: 2.5em;
-        }
-
-        .filters {
-            margin-bottom: 10px;
-        }
-
-        #timeseries { float: left; }
-        #timeseriesLegend { margin-left: 30px; }
-        #timeline { float: left; clear: left; }
-        #main { margin-top: 30px; }
-    </style>
-
-  </head>
-  <body>
-    <!-- Many thanks to the examples that helped with this visualization:
-        http://bl.ocks.org/kerryrodden/7090426
-        http://bl.ocks.org/kerryrodden/477c1bfb081b783f80ad
-    !-->
-    <section class='dynamic'>
-        <section class='filters'>
-            <label>From
-                <select data-bind='options: dates, value: fromDate'></select>
-            </label>
-            <label>To
-                <select data-bind='options: toDates, value: toDate'></select>
-            </label>
-            <label>Wiki
-                <select data-bind='options: availableWikis, value: 
wiki'></select>
-            </label>
-        </section>
-    </section>
-    <div id='timeseries'></div>
-    <div id='timeseriesLegend'></div>
-    <div id='timeline'></div>
-    <div id='main'>
-      <div id='sequence'></div>
-      <div id='chart'>
-        <div id='explanation' style='visibility: hidden;'>
-          <span id='percentage'></span><br/>
-          of sessions
-          <span id='ratio'></span>
-        </div>
-      </div>
-    </div>
-    <div id='sidebar'>
-      <div id='legend'></div>
-    </div>
-    <script type='text/javascript' src='util.js'></script>
-    <script type='text/javascript' src='funnel-prototype.js'></script>
-  </body>
-</html>
diff --git a/static/funnel-prototype.js b/static/funnel-prototype.js
deleted file mode 100644
index 809e77f..0000000
--- a/static/funnel-prototype.js
+++ /dev/null
@@ -1,589 +0,0 @@
-var ALL = '* All *';
-
-// Mapping of step names to colors.
-var colors = {
-    'init': '#5687d1',
-    'ready': '#7b615c',
-    'saveIntent': '#de783b',
-    'saveAttempt': '#17becf',
-    'saveSuccess': '#6ab975',
-    'saveFailure': '#a173d1',
-    'abort': '#bcbd22',
-    'end': '#bbbbbb'
-};
-
-var timeColors = d3.scale.ordinal().domain([
-    'Bounce Rate',
-    'Save Not Attempted Rate',
-    'Save Failure Rate',
-    'Save Success Rate',
-]).range([
-    colors.abort,
-    colors.ready,
-    colors.saveFailure,
-    colors.saveSuccess,
-]);
-
-var viewModel = {
-    dates: ko.observableArray([]),
-    fromDate: ko.observable(),
-    toDate: ko.observable(),
-
-    wikis: ko.observable(),
-    wiki: ko.observable(),
-
-    rawData: ko.observable([])
-};
-viewModel.toDates = ko.computed(function() {
-    return this.dates().filter(function (d) {
-        return d >= this.fromDate();
-    }, this);
-}, viewModel);
-
-viewModel.availableWikis = ko.computed(function () {
-    var wikis = this.wikis();
-    if (!wikis) { return []; }
-
-    var wikiNames = Object.getOwnPropertyNames(wikis),
-        toDates = this.toDates(),
-        to = this.toDate();
-
-    var wikisWithData = {};
-    for (var w = 0; w < wikiNames.length; w++) {
-        var wiki = wikiNames[w];
-        var datesWithData = Object.getOwnPropertyNames(wikis[wiki]);
-        for (var i = 0; i < toDates.length && toDates[i] <= to; i++) {
-            var found = datesWithData.find(function (date) {
-                return date === toDates[i];
-            });
-            if (found) {
-                wikisWithData[wiki] = true;
-            }
-        }
-    }
-    wikisWithData = Object.getOwnPropertyNames(wikisWithData).sort();
-
-    wikisWithData.push(ALL);
-    return wikisWithData;
-}, viewModel);
-
-viewModel.filteredData = ko.computed(function () {
-    var from = this.fromDate(),
-        to = this.toDate(),
-        wiki = this.wiki(),
-        all = wiki === ALL,
-        raw = this.rawData();
-
-    var filtered = [];
-    // NOTE: processing with plain loop because
-    // raw.filter(...) caused mayhem and death
-    for (var i=0; i<raw.length; i++) {
-        var row = raw[i];
-        if (   (from <= row[0] && row[0] <= to)
-            && (all || row[1] === wiki)
-        ) {
-            filtered.push(row);
-        }
-    }
-    return filtered;
-// delay this until the dependencies are set up
-}, viewModel).extend({ rateLimit: 1 });
-
-var first = true;
-viewModel.filteredData.subscribe(function (rows) {
-    var tree = buildHierarchy(rows);
-    vis.selectAll('path').remove();
-    if (first) {
-        initializeVisualization();
-        first = false;
-    }
-    createStarburst(tree);
-
-}, viewModel);
-
-viewModel.timeseriesData = ko.computed(function () {
-    var series = {},
-        filteredData = this.filteredData() || [];
-
-    // performance tests on regex vs. plain string operations are inconclusive:
-    // http://jsperf.com/negative-lookahead-vs-index-of/2
-    // TODO: in SQL, filter out weird cases like:
-    //     * success without attempt
-    //     * success before attempt
-    //     * in general, events in the wrong order
-    // otherwise, this analysis will have mismatching divisors and dividends
-    series.bounce = {
-        name: 'Bounce Rate',
-        dividendTest: 
/^((?!(success|failure|attempt|intent)).)*ready((?!(success|failure|attempt|intent)).)*$/i,
-        divisorTest: /^((?!(success|failure|attempt|intent)).)*ready/i,
-
-        dividendFunc: function (path) {
-            var str = path.toLowerCase();
-            return str.indexOf('ready') >= 0
-                && str.indexOf('success') < 0
-                && str.indexOf('failure') < 0
-                && str.indexOf('attempt') < 0
-                && str.indexOf('intent') < 0;
-        },
-        divisorFunc: function (path) {
-            var str = path.toLowerCase();
-            return str.indexOf('ready') >= 0;
-        },
-    };
-    series.attempt = {
-        name: 'Save Not Attempted Rate',
-        dividendTest: /^((?!attempt).)*ready((?!attempt).)*$/i,
-        divisorTest: /^((?!attempt).)*ready/i,
-
-        dividendFunc: function (path) {
-            var str = path.toLowerCase();
-            return str.indexOf('ready') >= 0
-                && str.indexOf('attempt') < 0;
-        },
-        divisorFunc: function (path) {
-            var str = path.toLowerCase();
-            return str.indexOf('ready') >= 0;
-        },
-    };
-    series.success = {
-        name: 'Save Failure Rate',
-        // NOTE: quite a bit lower than measuring # failures / # attempts,
-        //   because, often, people succeed after failing
-        dividendTest: /^((?!success).)*attempt((?!success).)*$/i,
-        divisorTest: /^((?!success).)*attempt/i,
-
-        dividendFunc: function (path) {
-            var str = path.toLowerCase();
-            return str.indexOf('failure') >= 0;
-        },
-        divisorFunc: function (path) {
-            var str = path.toLowerCase();
-            return str.indexOf('attempt') >= 0;
-        },
-    };
-    series.failure = {
-        name: 'Save Success Rate',
-        dividendTest: /^((?!success).)*attempt.*success/i,
-        divisorTest: /^((?!success).)*attempt/i,
-
-        dividendFunc: function (path) {
-            var str = path.toLowerCase();
-            return str.indexOf('success') >= 0;
-        },
-        divisorFunc: function (path) {
-            var str = path.toLowerCase();
-            return str.indexOf('attempt') >= 0;
-        },
-    };
-
-    var serieKeys = Object.getOwnPropertyNames(series);
-    serieKeys.forEach(function (key) {
-        series[key].data = [];
-        series[key].scratch = {};
-        series[key].color = timeColors(series[key].name);
-    });
-
-    for (var i=0; i<filteredData.length; i++) {
-        // FIXME: this parsing should happen on load or anywhere the data is 
iterated
-        var row = filteredData[i],
-            mili = Date.parse(row[0]),
-            count = parseInt(row[3]);
-
-        serieKeys.forEach(function (key) {
-            var serie = series[key];
-            if (!serie.scratch.hasOwnProperty(mili)) {
-                serie.scratch[mili] = {
-                    dividend: 0,
-                    divisor: 0,
-                };
-            }
-            series[key].scratch[mili].divisor += 
serie.divisorTest.test(row[2]) ? count : 0;
-            series[key].scratch[mili].dividend += 
serie.dividendTest.test(row[2]) ? count : 0;
-        });
-    }
-
-    var output = [];
-    serieKeys.forEach(function (key) {
-        var serie = series[key],
-            days = Object.getOwnPropertyNames(serie.scratch).sort();
-
-        serie.data.push.apply(serie.data, days.map(function (mili) {
-            var val = serie.scratch[mili];
-            return {
-                x: parseInt(mili) / 1000,
-                y: val.divisor === 0 ? 0 : val.dividend / val.divisor
-            };
-        }));
-        output.push(serie);
-    });
-
-    return output;
-}, viewModel).extend({ rateLimit: 1 });
-
-var metrics = viewModel.timeseriesData();
-var graph = createTimeseries(metrics);
-
-viewModel.timeseriesData.subscribe(function (series) {
-    series.forEach(function (serie) {
-        var metric = metrics.find(function (metric) {
-            return metric.name === serie.name;
-        });
-        metric.data.length = 0;
-        metric.data.push.apply(metric.data, serie.data);
-    });
-
-    graph.update();
-    if (!graph.annotator) {
-        addAnnotations(graph);
-    }
-});
-
-ko.applyBindings(viewModel, $('.dynamic')[0]);
-
-
-// ** Sunburst logic
-// Dimensions of sunburst.
-var width = 850;
-var height = 600;
-var radius = Math.min(width, height) / 2;
-
-// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
-var b = {
-    w: 75, h: 30, s: 3, t: 10
-};
-
-// Total size of all segments; we set this later, after loading the data.
-var totalSize = 0;
-
-var vis = d3.select('#chart').append('svg:svg')
-    .attr('width', width)
-    .attr('height', height)
-    .append('svg:g')
-    .attr('id', 'container')
-    .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
-
-var partition = d3.layout.partition()
-    .size([2 * Math.PI, radius * radius])
-    .value(function(d) { return d.size; });
-
-var arc = d3.svg.arc()
-    .startAngle(function(d) { return d.x; })
-    .endAngle(function(d) { return d.x + d.dx; })
-    .innerRadius(function(d) { return Math.sqrt(d.y); })
-    .outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
-
-// Use d3.text and d3.tsv.parseRows so that we do not need to have a header
-// row, and can receive the tsv as an array of arrays.
-// to get the data for this, see one of:
-//   http://tools-static.wmflabs.org/wikimetrics/ve-itwiki-actions.tsv
-//   http://tools-static.wmflabs.org/wikimetrics/ve-actions.tsv
-d3.text('ve-actions.tsv', function(text) {
-    var tsv = d3.tsv.parseRows(text);
-    var dates = {};
-    var wikis = {};
-    var rows = [];
-    tsv.forEach(function (row) {
-        if (isNaN(new Date(row[0]).getTime())) { return; }
-        dates[row[0]] = true;
-        wikis[row[1]] = wikis[row[1]] || {};
-        // capture which dates each wiki has access to
-        wikis[row[1]][row[0]] = true;
-        rows.push(row);
-    });
-    viewModel.dates(Object.getOwnPropertyNames(dates).sort());
-    viewModel.wikis(wikis);
-    var minMax = d3.extent(viewModel.dates());
-    viewModel.fromDate(minMax[0]);
-    viewModel.toDate(minMax[1]);
-    viewModel.wiki(ALL);
-    viewModel.rawData(rows);
-});
-
-function addAnnotations(graph) {
-    d3.text('ve-deployments.tsv', function(text) {
-        var tsv = d3.tsv.parseRows(text);
-
-        var annotator = new Rickshaw.Graph.Annotate({
-            graph: graph,
-            element: document.getElementById('timeline')
-        });
-
-        graph.annotator = annotator;
-
-        tsv.forEach(function (row) {
-            graph.annotator.add(
-                Date.parse(row[0]) / 1000,
-                // FIXME: obvious injection point
-                '<strong>' + row[0] + '</strong><br/><br/> ' + row[1]
-            );
-        });
-        graph.annotator.update();
-    });
-}
-
-// initialize visualization
-function initializeVisualization() {
-
-    // Basic setup of page elements.
-    initializeBreadcrumbTrail();
-    drawLegend();
-
-    // Bounding circle underneath the sunburst, to make it easier to detect
-    // when the mouse leaves the parent g.
-    vis.append('svg:circle')
-        .attr('r', radius)
-        .style('opacity', 0);
-
-    // Add the mouseleave handler to the bounding circle.
-    d3.select('#container').on('mouseleave', mouseleave);
-}
-// Main function to draw and set up the visualization, once we have the data.
-function createStarburst(json) {
-
-    // For efficiency, filter nodes to keep only those large enough to see.
-    var nodes = [],
-        allNodes = partition.nodes(json);
-
-    for (var i = 0; i < allNodes.length; i++) {
-        var node = allNodes[i];
-        if (node.dx > 0.005) { // 0.005 radians = 0.29 degrees
-            nodes.push(node);
-        }
-    }
-
-    var path = vis.data([json]).selectAll('path')
-        .data(nodes)
-        .enter().append('svg:path')
-        .attr('display', function(d) { return d.depth ? null : 'none'; })
-        .attr('d', arc)
-        .attr('fill-rule', 'evenodd')
-        .style('fill', function(d) { return colors[d.name]; })
-        .style('opacity', 1)
-        .on('mouseover', mouseover);
-
-    // Get total size of the tree = value of root node from partition.
-    totalSize = path.node().__data__.value;
-}
-
-// Fade all but the current sequence, and show it in the breadcrumb trail.
-function mouseover(d) {
-
-    var percentage = (100 * d.value / totalSize).toPrecision(3);
-    var percentageString = percentage + '%';
-    if (percentage < 0.1) {
-        percentageString = '< 0.1%';
-    }
-
-    d3.select('#percentage')
-        .text(percentageString);
-    d3.select('#explanation')
-        .style('visibility', '');
-    d3.select('#ratio')
-        .text('(' + d.value + ' out of ' + totalSize + ')');
-
-    var sequenceArray = getAncestors(d);
-    updateBreadcrumbs(sequenceArray, percentageString);
-
-    // Fade all the segments.
-    d3.selectAll('path')
-        .style('opacity', 0.3);
-
-    // Then highlight only those that are an ancestor of the current segment.
-    vis.selectAll('path')
-        .filter(function(node) {
-            return (sequenceArray.indexOf(node) >= 0);
-        })
-        .style('opacity', 1);
-}
-
-// Restore everything to full opacity when moving off the visualization.
-function mouseleave(d) {
-
-    // Hide the breadcrumb trail
-    d3.select('#trail')
-        .style('visibility', 'hidden');
-
-    // Deactivate all segments during transition.
-    d3.selectAll('path').on('mouseover', null);
-
-    // Transition each segment to full opacity and then reactivate it.
-    d3.selectAll('path')
-        .transition()
-        .duration(1000)
-        .style('opacity', 1)
-        .each('end', function() {
-            d3.select(this).on('mouseover', mouseover);
-        });
-
-    d3.select('#explanation')
-        .style('visibility', 'hidden');
-}
-
-// Given a node in a partition layout, return an array of all of its ancestor
-// nodes, highest first, but excluding the root.
-function getAncestors(node) {
-    var path = [];
-    var current = node;
-    while (current.parent) {
-        path.unshift(current);
-        current = current.parent;
-    }
-    return path;
-}
-
-function initializeBreadcrumbTrail() {
-    // Add the svg area.
-    var trail = d3.select('#sequence').append('svg:svg')
-        .attr('width', width)
-        .attr('height', 50)
-        .attr('id', 'trail');
-    // Add the label at the end, for the percentage.
-    trail.append('svg:text')
-        .attr('id', 'endlabel')
-        .style('fill', '#000');
-}
-
-// Generate a string that describes the points of a breadcrumb polygon.
-function breadcrumbPoints(d, i) {
-    var points = [];
-    points.push('0,0');
-    points.push(b.w + ',0');
-    points.push(b.w + b.t + ',' + (b.h / 2));
-    points.push(b.w + ',' + b.h);
-    points.push('0,' + b.h);
-    if (i > 0) { // Leftmost breadcrumb
-        points.push(b.t + ',' + (b.h / 2));
-    }
-    return points.join(' ');
-}
-
-// Update the breadcrumb trail to show the current sequence and percentage.
-function updateBreadcrumbs(nodeArray, percentageString) {
-
-    // Data join; key function combines name and depth (= position in 
sequence).
-    var g = d3.select('#trail')
-        .selectAll('g')
-        .data(nodeArray, function(d) { return d.name + d.depth; });
-
-    // Add breadcrumb and label for entering nodes.
-    var entering = g.enter().append('svg:g');
-
-    entering.append('svg:polygon')
-        .attr('points', breadcrumbPoints)
-        .style('fill', function(d) { return colors[d.name]; });
-
-    entering.append('svg:text')
-        .attr('x', (b.w + b.t) / 2)
-        .attr('y', b.h / 2)
-        .attr('dy', '0.35em')
-        .attr('text-anchor', 'middle')
-        .text(function(d) { return d.name.replace('save', '').toLowerCase(); 
});
-
-    // Set position for entering and updating nodes.
-    g.attr('transform', function(d, i) {
-        return 'translate(' + i * (b.w + b.s) + ', 0)';
-    });
-
-    // Remove exiting nodes.
-    g.exit().remove();
-
-    // Now move and update the percentage at the end.
-    d3.select('#trail').select('#endlabel')
-        .attr('x', (nodeArray.length + 0.5) * (b.w + b.s))
-        .attr('y', b.h / 2)
-        .attr('dy', '0.35em')
-        .attr('text-anchor', 'middle')
-        .text(percentageString);
-
-    // Make the breadcrumb trail visible, if it's hidden.
-    d3.select('#trail')
-        .style('visibility', '');
-
-}
-
-function drawLegend() {
-
-    // Dimensions of legend item: width, height, spacing, radius of rounded 
rect.
-    var li = {
-        w: 75, h: 30, s: 3, r: 3
-    };
-
-    var legend = d3.select('#legend').append('svg:svg')
-        .attr('width', li.w)
-        .attr('height', d3.keys(colors).length * (li.h + li.s));
-
-    var g = legend.selectAll('g')
-        .data(d3.entries(colors))
-        .enter().append('svg:g')
-        .attr('transform', function(d, i) {
-            return 'translate(0,' + i * (li.h + li.s) + ')';
-        });
-
-    g.append('svg:rect')
-        .attr('rx', li.r)
-        .attr('ry', li.r)
-        .attr('width', li.w)
-        .attr('height', li.h)
-        .style('fill', function(d) { return d.value; });
-
-    g.append('svg:text')
-        .attr('x', li.w / 2)
-        .attr('y', li.h / 2)
-        .attr('dy', '0.35em')
-        .attr('text-anchor', 'middle')
-        .text(function(d) { return d.key.replace('save', '').toLowerCase(); });
-}
-
-// Take a 4-column CSV and transform it into a hierarchical structure suitable
-// for a partition layout. First two columns are the day and wiki observed.
-// The third column is a sequence of step names, from root to leaf, separated
-// by hyphens. The fourth column is a count of how often that sequence 
occurred.
-// Aggregates sizes from similar paths and handles end cases properly.
-function buildHierarchy(tsv) {
-    var root = {name: 'root', children: []};
-    tsv.forEach(function (row) {
-        var parts = row[2].split('-');
-            size = +row[3],
-            currentNode = root;
-
-        if (isNaN(size)) { return; }
-
-        // limit path length to 10
-        parts = parts.slice(0, 10);
-
-        // Always end the path because if the partition layout saw these paths:
-        //   A -> B -> C
-        //   A -> B -> C -> D
-        // it would not show the first one since C has other children.  So C 
needs
-        // an "end" leaf child.  We can look into simulating this but it's 
harder
-        parts.push('end');
-
-        parts.forEach(function (name, i) {
-            var last = i === parts.length - 1,
-                existing = currentNode.children &&
-                           currentNode.children.find(function (n) {
-                               return n.name === name;
-                           });
-
-            if (existing) {
-                if (last) {
-                    existing.size = (existing.size || 0) + size;
-                } else {
-                    currentNode = existing;
-                }
-            } else {
-                var nextNode = {name: name};
-                currentNode.children = currentNode.children || [];
-                currentNode.children.push(nextNode);
-
-                if (last) {
-                    nextNode.size = size;
-                } else {
-                    nextNode.children = [];
-                    currentNode = nextNode;
-                }
-            }
-        });
-    });
-    return root;
-}
diff --git a/static/stacked-bar-prototype.html 
b/static/stacked-bar-prototype.html
deleted file mode 100644
index e372bb3..0000000
--- a/static/stacked-bar-prototype.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<html>
-    <head>
-        <meta charset='utf-8'>
-        <title>Visual Editor - Failure Types by User Types</title>
-        <script src="http://d3js.org/d3.v3.min.js";></script>
-        <style>
-
-        body {
-          font: 13px sans-serif;
-          font-weight: bold;
-          fill: #444;
-        }
-
-        .axis path {
-          fill: none;
-          stroke: #444;
-          shape-rendering: crispEdges;
-        }
-
-        .clickButton {
-            cursor: pointer;
-            text-decoration: normal;
-            font-weight: normal;
-        }
-        .clickButton text:hover {
-            text-decoration: underline;
-        }
-
-        .x.axis path {
-          display: none;
-        }
-
-        .tooltip{
-            text-anchor: middle;
-        }
-
-        </style>
-    </head>
-    <body></body>
-    <script type='text/javascript' src='stacked-bar-prototype.js'></script>
-</html>
diff --git a/static/stacked-bar-prototype.js b/static/stacked-bar-prototype.js
deleted file mode 100644
index 159c27b..0000000
--- a/static/stacked-bar-prototype.js
+++ /dev/null
@@ -1,295 +0,0 @@
-// This code is based on this example:
-// http://bl.ocks.org/yuuniverse4444/8325617
-// Thanks yuuniverse4444
-
-var margin = {top: 20, right: 20, bottom: 30, left: 60},
-    legend_width = 180,
-    width = 500 - margin.left - margin.right + legend_width,
-    height = 500 - margin.top - margin.bottom;
-
-var x = d3.scale.ordinal().rangeRoundBands([0, width], .1),
-    yAbsolute = d3.scale.linear().rangeRound([height, 0]),
-    yRelative = d3.scale.linear().rangeRound([height, 0]);
-
-var color = d3.scale.ordinal()
-    .range(["#a5c5d5", "#88a1b3", "#967784", "#b06474",
-        "#b27476", "#b98574", "#cd8c65", "#cdad55", "#b4ac54"]);
-
-var xAxis = d3.svg.axis().scale(x).orient("bottom");
-
-var yAxisRelative = d3.svg.axis()
-    .scale(yRelative)
-    .orient("left")
-    .tickFormat(d3.format(".0%"));
-
-var yAxisAbsolute = d3.svg.axis()
-    .scale(yAbsolute)
-    .orient("left")
-    .tickFormat(d3.format("2s"));
-
-var svg = d3.select("body").append("svg")
-    .attr("width", width + margin.left + margin.right + legend_width)
-    .attr("height", height + margin.top + margin.bottom)
-    .append("g")
-    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
-
-if (location.hash) {
-    // Get data URI from the hash
-    var dataURI = location.hash.substring(1);
-    d3.csv(dataURI, function(error, data) {
-
-        // TODO: Refactor this method to not use a hardcoded column name
-        color.domain(d3.keys(data[0]).filter(function(key) { return key !== 
"State"; }));
-
-        data.forEach(function(d) {
-            var mystate = d.State;
-            var y0 = 0;
-            d.columns = color.domain().map(function(name) {
-                return {mystate:mystate, name: name, y0: y0, y1: y0 += 
+d[name]};
-            });
-
-            d.total = d.columns[d.columns.length - 1].y1; // the last row
-            d.pct = [];
-
-            for (var i=0;i <d.columns.length;i ++ ){
-
-                var y_coordinate = +d.columns[i].y1/d.total;
-                var y_height1 = (d.columns[i].y1)/d.total;
-                var y_height0 = (d.columns[i].y0)/d.total;
-                var y_pct = y_height1 - y_height0;
-                d.pct.push({
-                    y_coordinate: y_coordinate,
-                    y_height1: y_height1,
-                    y_height0: y_height0,
-                    name: d.columns[i].name,
-                    mystate: d.State,
-                    y_pct: y_pct
-                });
-            }
-        });
-
-        x.domain(data.map(function(d) { return d.State; }));
-        yAbsolute.domain([0, d3.max(data, function(d) { return d.total; })]); 
// Absolute View scale
-        yRelative.domain([0,1]); // Relative View domain
-
-        var absoluteView = false; // define a boolean variable, true is 
absolute view,
-                                  // false is relative view. Initial view is 
absolute
-
-        svg.append("g")
-            .attr("class", "x axis")
-            .attr("transform", "translate(0," + height + ")")
-            .call(xAxis);
-
-        //Define the rect of Relative
-
-        var stateRelative = svg.selectAll(".relative")
-            .data(data)
-            .enter().append("g")
-            .attr("class", "relative")
-            .attr("transform", function(d) {
-                return "translate(" + "0 "+ ",0)";
-        });
-
-        stateRelative.selectAll("rect")
-            .data(function(d) {
-                return d.pct;
-            })
-            .enter().append("rect")
-            .attr("width", x.rangeBand())
-            .attr("y", function(d) {
-                return yRelative(d.y_coordinate);
-            })
-            .attr("x",function(d) {return x(d.mystate)})
-            .attr("height", function(d) {
-                return yRelative(d.y_height0) - yRelative(d.y_height1); 
//distance
-            })
-            .attr("fill", function(d){return color(d.name)})
-            .attr("stroke","pink")
-            .attr("stroke-width",0.2)
-            .attr("id",function(d) {return d.mystate})
-            .attr("class","relative")
-            .attr("id",function(d) {return d.mystate})
-            .style("pointer-events","visible");
-
-        stateRelative.selectAll("rect")
-            .on("mouseover", function(d){
-                var xPos = parseFloat(d3.select(this).attr("x"));
-                var yPos = parseFloat(d3.select(this).attr("y"));
-                var height = parseFloat(d3.select(this).attr("height"))
-                var width = parseFloat(d3.select(this).attr("width"))
-
-                d.previousFill = d3.select(this).attr("fill");
-                var newFill = highlightColor(d.previousFill, 10);
-                d3.select(this).attr("fill", newFill);
-
-                svg.append("text")
-                    .attr("x",xPos + width/2)
-                    .attr("y",yPos + height/2)
-                    .attr("class","tooltip")
-                    .style("pointer-events","none")
-                    .text(Math.floor(d.y_pct.toFixed(2)*100) + "%");
-            })
-            .on("mouseout",function(d){
-                svg.select(".tooltip").remove();
-                d3.select(this).attr("fill",d.previousFill);
-            });
-
-        // End of define rect of relative
-
-        // define rect for absolute
-
-        var stateAbsolute= svg.selectAll(".absolute")
-            .data(data)
-            .enter().append("g")
-            .attr("class", "absolute")
-            .attr("transform", function(d) { return "translate(" + "0" + 
",0)"; });
-
-        stateAbsolute.selectAll("rect")
-            .data(function(d) { return d.columns})
-            .enter().append("rect")
-            .attr("width", x.rangeBand())
-            .attr("y", function(d) {
-
-                  return yAbsolute(d.y1);
-            })
-            .attr("x",function(d) {
-                  return x(d.mystate)
-            })
-            .attr("height", function(d) {
-                  return yAbsolute(d.y0) - yAbsolute(d.y1);
-                  })
-            .attr("fill", function(d){
-                  return color(d.name)
-                  })
-            .attr("id",function(d) {
-                  return d.mystate
-            })
-            .attr("class","absolute")
-            .style("pointer-events","visible")
-            .attr("opacity",0) // initially it is invisible, i.e. start with 
Absolute View
-            .on("mouseover", function(d){
-                var xPos = parseFloat(d3.select(this).attr("x"));
-                var yPos = parseFloat(d3.select(this).attr("y"));
-                var height = parseFloat(d3.select(this).attr("height"))
-                var width = parseFloat(d3.select(this).attr("width"))
-
-                d.previousFill = d3.select(this).attr("fill");
-                var newFill = highlightColor(d.previousFill, 10);
-                d3.select(this).attr("fill", newFill);
-
-                svg.append("text")
-                    .attr("x",xPos + width/2)
-                    .attr("y",yPos + height/2)
-                    .attr("class","tooltip")
-                    .style("pointer-events","none")
-                    .text(Math.floor((d.y1-d.y0).toFixed(2)));
-            })
-            .on("mouseout",function(d){
-                svg.select(".tooltip").remove();
-                d3.select(this).attr("fill",d.previousFill);
-            });
-
-        //define two different scales, but one of them will always be hidden.
-        svg.append("g")
-            .attr("class", "y axis absolute")
-            .call(yAxisAbsolute)
-            .append("text")
-            .attr("transform", "rotate(-90)")
-            .attr("y", 6)
-            .attr("dy", ".71em")
-            .style("text-anchor", "end");
-
-        svg.append("g")
-            .attr("class", "y axis relative")
-            .call(yAxisRelative)
-            .append("text")
-            .attr("transform", "rotate(-90)")
-            .attr("y", 6)
-            .attr("dy", ".71em")
-            .style("text-anchor", "end");
-
-        svg.select(".y.axis.absolute").style("opacity",0);
-
-        // end of define absolute
-
-        // adding legend
-        var legend = svg.selectAll(".legend")
-            .data(color.domain().slice().reverse())
-            .enter().append("g")
-            .attr("class", "legend")
-            .attr("transform", function(d, i) { return "translate(0," + i * 20 
+ ")"; });
-
-        legend.append("rect")
-            .attr("x", width - 18+legend_width)
-            .attr("width", 18)
-            .attr("height", 18)
-            .attr("fill", color);
-
-        legend.append("text")
-            .attr("x", width - 24+legend_width)
-            .attr("y", 9)
-            .attr("dy", ".35em")
-            .style("text-anchor", "end")
-            .text(function(d) { return d; });
-
-        var clickButton = svg.selectAll(".clickButton")
-            .data([30,30])
-            .enter().append("g")
-            .attr("class","clickButton");
-
-        clickButton.append("text")
-            .attr("x", width +legend_width)
-            .attr("y", 265)
-            .attr("dy", ".35em")
-            .style("text-anchor", "end")
-            .text("Switch View")
-            .style("font-size", "14px")
-            .attr("fill","blue")
-            .attr("id","clickChangeView");
-
-        // start with relative view
-        Transition2Relative();
-
-        // Switch view on click the clickButton
-        d3.selectAll("#"+ "clickChangeView")
-            .on("click",function(){
-                if(absoluteView){ // absolute, otherwise relative
-                    Transition2Relative();
-                } else {
-                    Transition2Absolute();
-                }
-                absoluteView = !absoluteView // change the current view status
-            });
-
-        function Transition2Absolute(){
-            // Currently it is Relative
-            
stateRelative.selectAll("rect").transition().style("opacity",0).style("visibility","hidden");
-            
stateAbsolute.selectAll("rect").transition().style("opacity",1).style("visibility","visible");
-            svg.select(".y.axis.relative").transition().style("opacity",0);
-            svg.select(".y.axis.absolute").transition().style("opacity",1);
-        }
-
-        function Transition2Relative(){
-            // Currently it is absolute
-            
stateAbsolute.selectAll("rect").transition().style("opacity",0).style("visibility","hidden");
-            
stateRelative.selectAll("rect").transition().style("opacity",1).style("visibility","visible");
-            svg.select(".y.axis.absolute").transition().style("opacity",0);
-            svg.select(".y.axis.relative").transition().style("opacity",1);
-        }
-
-        // This code is taken from:
-        // 
http://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors
-        // Thanks Pimp Trizkit
-        function highlightColor(color, percent){
-            var num = parseInt(color.slice(1), 16),
-                amt = Math.round(2.55 * percent),
-                R = (num >> 16) + amt,
-                G = (num >> 8 & 0x00FF) + amt,
-                B = (num & 0x0000FF) + amt;
-
-            return ("#" + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 
0x10000 +
-                (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + ( B < 255 ? B < 1 ? 
0 : B : 255)
-                ).toString(16).slice(1));
-        }
-    });
-}
diff --git a/static/util.js b/static/util.js
deleted file mode 100644
index 70ea8d8..0000000
--- a/static/util.js
+++ /dev/null
@@ -1,69 +0,0 @@
-// ** Rickshaw logic
-// only call once, it returns a graph that you can call update on
-function createTimeseries(metrics, opt) {
-    var opt = opt || {};
-
-    var graph = new Rickshaw.Graph({
-        element: document.getElementById('timeseries'),
-        width: opt.width || 700,
-        height: opt.height || 300,
-        renderer: 'line',
-        series: metrics
-    });
-
-    var xAxis = new Rickshaw.Graph.Axis.Time({ graph: graph });
-    var yAxis = new Rickshaw.Graph.Axis.Y({
-        graph: graph,
-        orientation: 'right',
-        tickFormat: Rickshaw.Fixtures.Number.formatKMBT
-    });
-
-    var legend = new Rickshaw.Graph.Legend({
-        graph: graph,
-        element: document.getElementById('timeseriesLegend')
-    });
-
-    var shelving = new Rickshaw.Graph.Behavior.Series.Toggle({
-        graph: graph,
-        legend: legend
-    });
-
-    var highlighter = new Rickshaw.Graph.Behavior.Series.Highlight({
-        graph: graph,
-        legend: legend
-    });
-
-    var hoverDetail = new Rickshaw.Graph.HoverDetail( {
-        graph: graph
-    } );
-
-    graph.render();
-    xAxis.render();
-    yAxis.render();
-
-    return graph;
-}
-
-if (!Array.prototype.find) {
-    Array.prototype.find = function(predicate) {
-        if (this == null) {
-            throw new TypeError('Array.prototype.find called on null or 
undefined');
-        }
-        if (typeof predicate !== 'function') {
-            throw new TypeError('predicate must be a function');
-        }
-        var list = Object(this);
-        var length = list.length >>> 0;
-        var thisArg = arguments[1];
-        var value;
-
-        for (var i = 0; i < length; i++) {
-            value = list[i];
-            if (predicate.call(thisArg, value, i, list)) {
-                return value;
-            }
-        }
-        return undefined;
-    };
-}
-function t(){ return (new Date()).getTime(); }

-- 
To view, visit https://gerrit.wikimedia.org/r/315829
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Id59e97dc650213f34207cbaacf538642815db658
Gerrit-PatchSet: 2
Gerrit-Project: analytics/limn-edit-data
Gerrit-Branch: master
Gerrit-Owner: Milimetric <dandree...@wikimedia.org>
Gerrit-Reviewer: Mforns <mfo...@wikimedia.org>
Gerrit-Reviewer: Milimetric <dandree...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to