Index: bin/install
===================================================================
--- bin/install	(revision 26220baec627ed3260e22377ce342737e9e88b57)
+++ bin/install	(revision ed7e9d705c8bba87f94569aedd075b0a76736d1d)
@@ -10,4 +10,12 @@
 # Full path to the env
 env_path=${current}/env
+
+
+set_scripts_permissions() {
+    # ensure the shell scripts we will need have proper permissions
+    chmod u+x ${current}/bin/js_deps
+    chmod u+x ${current}/bin/start
+    chmod u+x ${current}/bin/screenshot_map
+}
 
 check_python3() {
@@ -55,10 +63,8 @@
 
 install_js_deps() {
-    chmod +x ${current}/bin/js_deps
     ${current}/bin/js_deps
 }
 
 setup_start_stop() {
-    chmod +x ${current}/bin/start
     echo "OpenWorkouts successfully installed in ${env_path}"
     echo ""
@@ -73,4 +79,5 @@
 }
 
+set_script_permissions
 check_python3
 create_venv
Index: ow/models/user.py
===================================================================
--- ow/models/user.py	(revision 26220baec627ed3260e22377ce342737e9e88b57)
+++ ow/models/user.py	(revision ed7e9d705c8bba87f94569aedd075b0a76736d1d)
@@ -234,2 +234,56 @@
                         timedelta())
         }
+
+    @property
+    def yearly_stats(self):
+        """
+        Return per-month stats for the last 12 months
+        """
+        # set the boundaries for looking for workouts afterwards,
+        # we need the current date as the "end date" and one year
+        # ago from that date. Then we set the start at the first
+        # day of that month.
+        end = datetime.now(timezone.utc)
+        start = (end - timedelta(days=365)).replace(day=1)
+
+        # build the stats, populating first the dict with empty values
+        # for each month.
+        stats = {}
+        for days in range((end - start).days):
+            day = (start + timedelta(days=days)).date()
+            if (day.year, day.month) not in stats.keys():
+                stats[(day.year, day.month)] = {
+                    'workouts': 0,
+                    'time': timedelta(0),
+                    'distance': Decimal(0),
+                    'elevation': Decimal(0),
+                    'sports': {}
+                }
+
+        # now loop over workouts, filtering and then adding stats to the
+        # proper place
+        for workout in self.workouts():
+            if start.date() <= workout.start.date() <= end.date():
+                # less typing, avoid long lines
+                month = stats[
+                    (workout.start.date().year, workout.start.date().month)]
+                month['workouts'] += 1
+                month['time'] += workout.duration or timedelta(seconds=0)
+                month['distance'] += workout.distance or Decimal(0)
+                month['elevation'] += workout.uphill or Decimal(0)
+                if workout.sport not in month['sports']:
+                    month['sports'][workout.sport] = {
+                        'workouts': 0,
+                        'time': timedelta(seconds=0),
+                        'distance': Decimal(0),
+                        'elevation': Decimal(0),
+                    }
+                month['sports'][workout.sport]['workouts'] += 1
+                month['sports'][workout.sport]['time'] += (
+                    workout.duration or timedelta(0))
+                month['sports'][workout.sport]['distance'] += (
+                    workout.distance or Decimal(0))
+                month['sports'][workout.sport]['elevation'] += (
+                    workout.uphill or Decimal(0))
+
+        return stats
Index: ow/static/js/ow.js
===================================================================
--- ow/static/js/ow.js	(revision 26220baec627ed3260e22377ce342737e9e88b57)
+++ ow/static/js/ow.js	(revision ed7e9d705c8bba87f94569aedd075b0a76736d1d)
@@ -31,5 +31,5 @@
     var openstreetmap_attr = 'Map data &copy; <a href="http://www.osm.org">OpenStreetMap</a>';
 
-    // Some constants reused through the code
+    // Some vars reused through the code
     var map;
     var gpx;
@@ -147,5 +147,5 @@
            Build a d3 bar chart, populated with data from the given url.
          */
-        var chart = d3.select("svg"),
+        var chart = d3.select(chart_selector),
             margin = {top: 20, right: 20, bottom: 30, left: 50},
             width = +chart.attr("width") - margin.left - margin.right,
@@ -227,2 +227,147 @@
 
 };
+
+
+owjs.year_chart = function(spec) {
+
+    "use strict";
+
+    // parameters provided when creating an "instance" of the chart
+    var chart_selector = spec.chart_selector,
+        filters_selector = spec.filters_selector,
+        url = spec.url,
+        current_month = spec.current_month,
+        y_axis_labels = spec.y_axis_labels;
+
+    // Helpers
+    function select_x_axis_label(d) {
+        /* Given a value, return the label associated with it */
+        return d3.select('.x-axis-b')
+    	    .selectAll('text')
+    	    .filter(function(x) { return x == d.name; });
+    };
+
+    function get_y_value(d, filter_by) {
+        return Number(d[filter_by]);
+    };
+
+    function get_y_axis_label(filter_by) {
+        return y_axis_labels[filter_by];
+    };
+
+    // Methods
+    var filters_setup = function filters_setup() {
+        $(filters_selector).on('click', function(e) {
+            var filter_by = 'distance';
+            e.preventDefault();
+            filter_by = $(this).attr('class').split('-')[1]
+            var chart = d3.select(chart_selector);
+            chart.selectAll("*").remove();
+            render(filter_by);
+        });
+    };
+
+    var render = function render(filter_by) {
+        /*
+          Build a d3 bar chart, populated with data from the given url.
+        */
+        var chart = d3.select(chart_selector),
+            margin = {top: 20, right: 20, bottom: 30, left: 50},
+            width = +chart.attr("width") - margin.left - margin.right,
+            height = +chart.attr("height") - margin.top - margin.bottom,
+            g = chart.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"),
+            x = d3.scaleBand().rangeRound([0, width]).padding(0.1),
+            y = d3.scaleLinear().rangeRound([height, 0]);
+
+        d3.json(url).then(function (data) {
+	    x.domain(data.map(function (d) {
+	        return d.name;
+	    }));
+
+	    y.domain([0, d3.max(data, function (d) {
+                return get_y_value(d, filter_by);
+	    })]);
+
+	    g.append("g")
+                .attr('class', 'x-axis-b')
+	        .attr("transform", "translate(0," + height + ")")
+                .call(d3.axisBottom(x))
+
+            g.append("g")
+	        .call(d3.axisLeft(y))
+	        .append("text")
+	        .attr("fill", "#000")
+	        .attr("transform", "rotate(-90)")
+	        .attr("y", 6)
+	        .attr("dy", "0.71em")
+	        .attr("text-anchor", "end")
+	        .text(get_y_axis_label(filter_by));
+
+	    g.selectAll(".bar")
+	        .data(data)
+	        .enter().append("rect")
+                .attr("class", function(d) {
+                    if (d.id == current_month){
+                        select_x_axis_label(d).attr('style', "font-weight: bold;");
+                        return 'bar current'
+                    }
+                    else {
+                        return 'bar'
+                    }
+                })
+	        .attr("x", function (d) {
+		    return x(d.name);
+	        })
+	        .attr("y", function (d) {
+		    return y(get_y_value(d, filter_by));
+	        })
+	        .attr("width", x.bandwidth())
+	        .attr("height", function (d) {
+		    return height - y(get_y_value(d, filter_by));
+	        })
+                .on('mouseover', function(d) {
+                    if (d.id != current_month){
+                        select_x_axis_label(d).attr('style', "font-weight: bold;");
+                    }
+  		})
+  		.on('mouseout', function(d) {
+                    if (d.id != current_month){
+        	        select_x_axis_label(d).attr('style', "font-weight: regular;");
+                    }
+                });
+
+            g.selectAll(".text")
+                .data(data)
+                .enter()
+                .append("text")
+                .attr("class","label")
+	        .attr("x", function (d) {
+		    return x(d.name) + x.bandwidth()/2;
+	        })
+	        .attr("y", function (d) {
+                    /*
+                      Get the value for the current bar, then get the maximum
+                      value to be displayed in the bar, which is used to
+                      calculate the proper position of the label for this bar,
+                      relatively to its height (1% above the bar)
+                     */
+                    var value = get_y_value(d, filter_by);
+                    var max = y.domain()[1];
+                    return y(value + y.domain()[1] * 0.01);
+	        })
+                .text(function(d) {
+                    var value = get_y_value(d, filter_by)
+                    if ( value > 0) {
+                        return value;
+                    }
+                });
+
+        });
+    };
+
+    var that = {}
+    that.filters_setup = filters_setup;
+    that.render = render;
+    return that
+
+};
Index: ow/templates/dashboard.pt
===================================================================
--- ow/templates/dashboard.pt	(revision 26220baec627ed3260e22377ce342737e9e88b57)
+++ ow/templates/dashboard.pt	(revision ed7e9d705c8bba87f94569aedd075b0a76736d1d)
@@ -211,5 +211,5 @@
     <script type="text/javascript">
      var week_chart = owjs.week_chart({
-         chart_selector: 'div.js-week-stats',
+         chart_selector: 'div.js-week-stats svg',
          url: "${request.resource_url(context, 'week')}",
          current_day_name: "${current_day_name}"
Index: ow/templates/profile.pt
===================================================================
--- ow/templates/profile.pt	(revision 26220baec627ed3260e22377ce342737e9e88b57)
+++ ow/templates/profile.pt	(revision ed7e9d705c8bba87f94569aedd075b0a76736d1d)
@@ -81,7 +81,41 @@
       </div>
 
+      <div class="month-stats js-month-stats">
+        <svg width="600" height="300"></svg>
+        <div class="filters js-filters">
+          <a href="#" class="js-distance" i18n:translate="">distance</a>
+          <a href="#" class="js-time" i18n:translate="">time</a>
+          <a href="#" class="js-elevation" i18n:translate="">elevation</a>
+        </div>
+      </div>
+
     </div>
 
   </metal:content>
 
+  <metal:body-js metal:fill-slot="body-js">
+
+    <script src="${request.static_url('ow:static/components/d3/d3.min.js')}"></script>
+    <script src="${request.static_url('ow:static/js/ow.js')}"></script>
+
+    <script type="text/javascript">
+     var y_axis_labels = {
+         "distance": "Kilometers",
+         "time": "Hours",
+         "elevation": "Meters"
+     };
+
+     var year_chart = owjs.year_chart({
+         chart_selector: 'div.js-month-stats svg',
+         filters_selector: 'div.js-month-stats div.js-filters a',
+         url: "${request.resource_url(context, 'yearly')}",
+         current_month: "${current_month}",
+         y_axis_labels: y_axis_labels,
+     });
+     year_chart.render("distance");
+     year_chart.filters_setup();
+    </script>
+
+  </metal:body-js>
+
 </html>
Index: ow/tests/views/test_user.py
===================================================================
--- ow/tests/views/test_user.py	(revision 26220baec627ed3260e22377ce342737e9e88b57)
+++ ow/tests/views/test_user.py	(revision ed7e9d705c8bba87f94569aedd075b0a76736d1d)
@@ -268,5 +268,7 @@
         request = dummy_request
         response = user_views.profile(john, request)
-        assert response == {}
+        assert len(response.keys()) == 1
+        current_month = datetime.now(timezone.utc).strftime('%Y-%m')
+        assert response['current_month'] == current_month
 
     def test_login_get(self, dummy_request):
Index: ow/views/user.py
===================================================================
--- ow/views/user.py	(revision 26220baec627ed3260e22377ce342737e9e88b57)
+++ ow/views/user.py	(revision ed7e9d705c8bba87f94569aedd075b0a76736d1d)
@@ -1,5 +1,5 @@
 import json
 from calendar import month_name
-from datetime import datetime
+from datetime import datetime, timezone
 
 from pyramid.httpexceptions import HTTPFound
@@ -146,6 +146,6 @@
 
     return {
-        'current_year': datetime.now().year,
-        'current_day_name': datetime.now().strftime('%a'),
+        'current_year': datetime.now(timezone.utc).year,
+        'current_day_name': datetime.now(timezone.utc).strftime('%a'),
         'month_name': month_name,
         'viewing_year': viewing_year,
@@ -165,5 +165,8 @@
     basic info, stats, etc
     """
-    return {}
+    now = datetime.now(timezone.utc)
+    return {
+        'current_month': now.strftime('%Y-%m')
+    }
 
 
@@ -250,2 +253,35 @@
                     charset='utf-8',
                     body=json.dumps(json_stats))
+
+
+@view_config(
+    context=User,
+    permission='view',
+    name='yearly')
+def last_months_stats(context, request):
+    """
+    Return a json-encoded stream with statistics for the last 12 months
+    """
+    stats = context.yearly_stats
+    # this sets which month is 2 times in the stats, once this year, once
+    # the previous year. We will show it a bit different in the UI (showing
+    # the year too to prevent confusion)
+    repeated_month = datetime.now(timezone.utc).date().month
+    json_stats = []
+    for month in stats:
+        hms = timedelta_to_hms(stats[month]['time'])
+        name = month_name[month[1]][:3]
+        if month[1] == repeated_month:
+            name += ' ' + str(month[0])
+        month_stats = {
+            'id': str(month[0]) + '-' + str(month[1]).zfill(2),
+            'name': name,
+            'time': str(hms[0]).zfill(2),
+            'distance': int(round(stats[month]['distance'])),
+            'elevation': int(stats[month]['elevation']),
+            'workouts': stats[month]['workouts']
+        }
+        json_stats.append(month_stats)
+    return Response(content_type='application/json',
+                    charset='utf-8',
+                    body=json.dumps(json_stats))
