Index: ow/models/user.py
===================================================================
--- ow/models/user.py	(revision 1183d5a78809fb8f75b4923e2d9f5eeabfde9cd6)
+++ ow/models/user.py	(revision 3357e47f25ed4e51fa708d2241f6690bc66daef0)
@@ -9,5 +9,5 @@
 
 from ow.catalog import get_catalog, reindex_object
-from ow.utilities import get_week_days
+from ow.utilities import get_week_days, get_month_week_number
 
 
@@ -77,5 +77,5 @@
         reindex_object(catalog, workout)
 
-    def workouts(self, year=None, month=None):
+    def workouts(self, year=None, month=None, week=None):
         """
         Return this user workouts, sorted by date, from newer to older
@@ -84,6 +84,10 @@
         if year:
             workouts = [w for w in workouts if w.start.year == year]
-        if month:
-            workouts = [w for w in workouts if w.start.month == month]
+            if month:
+                workouts = [w for w in workouts if w.start.month == month]
+            if week:
+                week = int(week)
+                workouts = [
+                    w for w in workouts if w.start.isocalendar()[1] == week]
         workouts = sorted(workouts, key=attrgetter('start'))
         workouts.reverse()
@@ -288,2 +292,65 @@
 
         return stats
+
+    @property
+    def weekly_year_stats(self):
+        """
+        Return per-week 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)
+
+        stats = {}
+
+        # first initialize the stats dict
+        for days in range((end - start).days):
+            day = (start + timedelta(days=days)).date()
+            week = day.isocalendar()[1]
+            month_week = get_month_week_number(day)
+            key = (day.year, day.month, week, month_week)
+            if key not in stats.keys():
+                stats[key] = {
+                    'workouts': 0,
+                    'time': timedelta(0),
+                    'distance': Decimal(0),
+                    'elevation': Decimal(0),
+                    'sports': {}
+                }
+
+        # now loop over the 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
+                start_date = workout.start.date()
+                week = start_date.isocalendar()[1]
+                month_week = get_month_week_number(start_date)
+                week = stats[(start_date.year,
+                              start_date.month,
+                              week,
+                              month_week)]
+
+                week['workouts'] += 1
+                week['time'] += workout.duration or timedelta(seconds=0)
+                week['distance'] += workout.distance or Decimal(0)
+                week['elevation'] += workout.uphill or Decimal(0)
+                if workout.sport not in week['sports']:
+                    week['sports'][workout.sport] = {
+                        'workouts': 0,
+                        'time': timedelta(seconds=0),
+                        'distance': Decimal(0),
+                        'elevation': Decimal(0),
+                    }
+                week['sports'][workout.sport]['workouts'] += 1
+                week['sports'][workout.sport]['time'] += (
+                    workout.duration or timedelta(0))
+                week['sports'][workout.sport]['distance'] += (
+                    workout.distance or Decimal(0))
+                week['sports'][workout.sport]['elevation'] += (
+                    workout.uphill or Decimal(0))
+
+        return stats
Index: ow/static/js/ow.js
===================================================================
--- ow/static/js/ow.js	(revision 1183d5a78809fb8f75b4923e2d9f5eeabfde9cd6)
+++ ow/static/js/ow.js	(revision 3357e47f25ed4e51fa708d2241f6690bc66daef0)
@@ -236,7 +236,11 @@
     var chart_selector = spec.chart_selector,
         filters_selector = spec.filters_selector,
-        url = spec.url,
+        switcher_selector = spec.switcher_selector,
+        urls = spec.urls,
         current_month = spec.current_month,
-        y_axis_labels = spec.y_axis_labels;
+        current_week = spec.current_week,
+        y_axis_labels = spec.y_axis_labels,
+        filter_by = spec.filter_by,
+        url = spec.url;
 
     // Helpers
@@ -256,17 +260,35 @@
     };
 
+    function get_name_for_x(d) {
+        if (d.week == undefined || d.week == 0) {
+            return d.name;
+        }
+        else {
+            return d.id.split('-')[2];
+        }
+    }
+
     // 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) {
+            render(filter_by, url);
+        });
+    };
+
+    var switcher_setup = function switcher_setup() {
+        $(switcher_selector).on('click', function(e) {
+            e.preventDefault();
+            url = $(this).attr('class').split('-')[1]
+            var chart = d3.select(chart_selector);
+            chart.selectAll("*").remove();
+            render(filter_by, url);
+        });
+    };
+
+    var render = function render(filter_by, url) {
         /*
           Build a d3 bar chart, populated with data from the given url.
@@ -280,7 +302,8 @@
             y = d3.scaleLinear().rangeRound([height, 0]);
 
-        d3.json(url).then(function (data) {
+        d3.json(urls[url]).then(function (data) {
 	    x.domain(data.map(function (d) {
-	        return d.name;
+                return get_name_for_x(d);
+	        // return d.name;
 	    }));
 
@@ -308,14 +331,29 @@
 	        .enter().append("rect")
                 .attr("class", function(d) {
-                    if (d.id == current_month){
+                    var sel_week = current_month + '-' + current_week;
+                    if (d.id == current_month || d.id == sel_week){
+                        /* Bar for the currently selected month or week */
                         select_x_axis_label(d).attr('style', "font-weight: bold;");
-                        return 'bar current'
+                        return 'bar current';
                     }
                     else {
-                        return 'bar'
+                        if (!current_week && d.id.indexOf(current_month) >=0 ) {
+                            /*
+                               User selected a month, then switched to weekly
+                               view, we do highlight all the bars for weeks in
+                               that month
+                            */
+                            select_x_axis_label(d).attr('style', "font-weight: bold;");
+                            return 'bar current';
+                        }
+                        else {
+                            /* Non-selected bar */
+                            return 'bar';
+                        }
+
                     }
                 })
 	        .attr("x", function (d) {
-		    return x(d.name);
+		    return x(get_name_for_x(d));
 	        })
 	        .attr("y", function (d) {
@@ -340,30 +378,46 @@
                 });
 
-            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;
-                    }
-                });
-
+            if (url == 'monthly') {
+                g.selectAll(".text")
+                    .data(data)
+                    .enter()
+                    .append("text")
+                    .attr("class","label")
+	            .attr("x", function (d) {
+		        return x(get_name_for_x(d)) + 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;
+                        }
+                    });
+            }
+
+            if (url == 'weekly') {
+                g.selectAll(".tick")
+                    .each(function (d, i) {
+                        /*
+                          Remove from the x-axis tickets those without letters
+                          on them (useful for the weekly chart)
+                        */
+                        if (d !== parseInt(d, 10)) {
+                            if(!d.match(/[a-z]/i)) {
+                                this.remove();
+                            }
+                        }
+                    });
+            }
         });
     };
@@ -371,4 +425,5 @@
     var that = {}
     that.filters_setup = filters_setup;
+    that.switcher_setup = switcher_setup;
     that.render = render;
     return that
Index: ow/templates/profile.pt
===================================================================
--- ow/templates/profile.pt	(revision 1183d5a78809fb8f75b4923e2d9f5eeabfde9cd6)
+++ ow/templates/profile.pt	(revision 3357e47f25ed4e51fa708d2241f6690bc66daef0)
@@ -72,4 +72,8 @@
           <a href="#" class="js-time" i18n:translate="">time</a>
           <a href="#" class="js-elevation" i18n:translate="">elevation</a>
+        </div>
+        <div class="switcher js-switcher">
+          <a href="#" class="js-weekly" i18n:translate="">weekly</a>
+          <a href="#" class="js-monthly" i18n:translate="">monthly</a>
         </div>
       </div>
@@ -154,10 +158,16 @@
          chart_selector: 'div.js-month-stats svg',
          filters_selector: 'div.js-month-stats div.js-filters a',
-         url: "${request.resource_url(context, 'yearly')}",
+         switcher_selector: 'div.js-month-stats div.js-switcher a',
+         urls: {"monthly": "${request.resource_url(context, 'monthly')}",
+                "weekly": "${request.resource_url(context, 'weekly')}"},
          current_month: "${current_month}",
+         current_week: "${current_week}",
          y_axis_labels: y_axis_labels,
+         filter_by: "distance",
+         url: "${'monthly' if current_week is None else 'weekly'}",
      });
-     year_chart.render("distance");
+     year_chart.render("distance", "${'monthly' if current_week is None else 'weekly'}");
      year_chart.filters_setup();
+     year_chart.switcher_setup();
     </script>
 
Index: ow/tests/views/test_user.py
===================================================================
--- ow/tests/views/test_user.py	(revision 1183d5a78809fb8f75b4923e2d9f5eeabfde9cd6)
+++ ow/tests/views/test_user.py	(revision 3357e47f25ed4e51fa708d2241f6690bc66daef0)
@@ -269,7 +269,8 @@
         # profile page for the current day (no workouts avalable)
         response = user_views.profile(john, request)
-        assert len(response.keys()) == 2
+        assert len(response.keys()) == 3
         current_month = datetime.now(timezone.utc).strftime('%Y-%m')
         assert response['current_month'] == current_month
+        assert response['current_week'] is None
         assert response['workouts'] == []
         # profile page for a previous date, that has workouts
@@ -277,6 +278,25 @@
         request.GET['month'] = 8
         response = user_views.profile(john, request)
-        assert len(response.keys()) == 2
+        assert len(response.keys()) == 3
         assert response['current_month'] == '2015-08'
+        assert response['current_week'] is None
+        assert response['workouts'] == john.workouts(2015, 8)
+        # same, passing a week, first on a week without workouts
+        request.GET['year'] = 2015
+        request.GET['month'] = 8
+        request.GET['week'] = 25
+        response = user_views.profile(john, request)
+        assert len(response.keys()) == 3
+        assert response['current_month'] == '2015-08'
+        assert response['current_week'] is 25
+        assert response['workouts'] == []
+        # now in a week with workoutss
+        request.GET['year'] = 2015
+        request.GET['month'] = 8
+        request.GET['week'] = 26
+        response = user_views.profile(john, request)
+        assert len(response.keys()) == 3
+        assert response['current_month'] == '2015-08'
+        assert response['current_week'] is 26
         assert response['workouts'] == john.workouts(2015, 8)
 
Index: ow/utilities.py
===================================================================
--- ow/utilities.py	(revision 1183d5a78809fb8f75b4923e2d9f5eeabfde9cd6)
+++ ow/utilities.py	(revision 3357e47f25ed4e51fa708d2241f6690bc66daef0)
@@ -2,4 +2,5 @@
 import os
 import logging
+import calendar
 import subprocess
 from datetime import datetime, timedelta
@@ -234,2 +235,14 @@
     week_days = [first_day + timedelta(days=i) for i in range(7)]
     return week_days
+
+
+def get_month_week_number(day):
+    """
+    Given a datetime object (day), return the number of week the day is
+    in the current month (week 1, 2, 3, etc)
+    """
+    weeks = calendar.monthcalendar(day.year, day.month)
+    for week in weeks:
+        if day.day in week:
+            return weeks.index(week)
+    return None
Index: ow/views/user.py
===================================================================
--- ow/views/user.py	(revision 1183d5a78809fb8f75b4923e2d9f5eeabfde9cd6)
+++ ow/views/user.py	(revision 3357e47f25ed4e51fa708d2241f6690bc66daef0)
@@ -168,8 +168,10 @@
     year = int(request.GET.get('year', now.year))
     month = int(request.GET.get('month', now.month))
+    week = request.GET.get('week', None)
     return {
-        'workouts': context.workouts(year, month),
+        'workouts': context.workouts(year, month, week),
         'current_month': '{year}-{month}'.format(
-            year=str(year), month=str(month).zfill(2))
+            year=str(year), month=str(month).zfill(2)),
+        'current_week': week
     }
 
@@ -262,5 +264,5 @@
     context=User,
     permission='view',
-    name='yearly')
+    name='monthly')
 def last_months_stats(context, request):
     """
@@ -294,2 +296,44 @@
                     charset='utf-8',
                     body=json.dumps(json_stats))
+
+
+@view_config(
+    context=User,
+    permission='view',
+    name='weekly')
+def last_weeks_stats(context, request):
+    """
+    Return a json-encoded stream with statistics for the last 12-months, but
+    in a per-week basis
+    """
+    stats = context.weekly_year_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 week in stats:
+        hms = timedelta_to_hms(stats[week]['time'])
+        name = month_name[week[1]][:3]
+        if week[1] == repeated_month:
+            name += ' ' + str(week[0])
+        week_stats = {
+            'id': '-'.join(
+                [str(week[0]), str(week[1]).zfill(2), str(week[2])]),
+            'week': str(week[3]),  # the number of week in the current month
+            'name': name,
+            'time': str(hms[0]).zfill(2),
+            'distance': int(round(stats[week]['distance'])),
+            'elevation': int(stats[week]['elevation']),
+            'workouts': stats[week]['workouts'],
+            'url': request.resource_url(
+                context, 'profile',
+                query={'year': str(week[0]),
+                       'month': str(week[1]),
+                       'week': str(week[2])},
+                anchor='workouts')
+        }
+        json_stats.append(week_stats)
+    return Response(content_type='application/json',
+                    charset='utf-8',
+                    body=json.dumps(json_stats))
