source: OpenWorkouts-current/ow/static/js/ow.js @ f2c9e20

current
Last change on this file since f2c9e20 was f2c9e20, checked in by Borja Lopez <borja@…>, 5 years ago

(#7) Show an empty calendar heatmap chart for months with 0 workout activity
in the user profile page.

  • Property mode set to 100644
File size: 25.4 KB
Line 
1
2/*
3
4  OpenWorkouts Javascript code
5
6*/
7
8
9// Namespace
10var owjs = {};
11
12
13owjs.map = function(spec) {
14
15    "use strict";
16
17    // parameters provided when creating an "instance" of a map
18    var map_id = spec.map_id;
19    var latitude = spec.latitude;
20    var longitude = spec.longitude;
21    var zoom = spec.zoom;
22    var gpx_url = spec.gpx_url;
23    var start_icon = spec.start_icon;
24    var end_icon = spec.end_icon;
25    var shadow = spec.shadow;
26    var elevation = spec.elevation;
27    var zoom_control = spec.zoom_control;
28
29    // OpenStreetMap urls and references
30    var openstreetmap_url = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
31    var openstreetmap_attr = 'Map data &copy; <a href="http://www.osm.org">OpenStreetMap</a>';
32
33    // Some vars reused through the code
34    var map;
35    var gpx;
36    var elevation;
37
38    var create_map = function create_map(latitude, longitude, zoom) {
39        /* Create a Leaflet map, set center point and add tiles */
40        map = L.map(map_id, {zoomControl: zoom_control});
41        map.setView([latitude, longitude], zoom);
42        var tile_layer = L.tileLayer(openstreetmap_url, {
43            attribution: openstreetmap_attr
44        });
45        tile_layer.addTo(map);
46    };
47
48    var add_elevation_chart = function add_elevation_chart() {
49        /*
50           Add the elevation chart support to the map.
51           This has to be called *after* create_map and *before* load_gpx.
52        */
53
54        elevation = L.control.elevation({
55            position: "bottomright",
56            theme: "openworkouts-theme",
57            useHeightIndicator: true, //if false a marker is drawn at map position
58            interpolation: d3.curveLinear,
59            elevationDiv: "#elevation",
60            detachedView: true,
61            responsiveView: true,
62            gpxOptions: {
63                async: true,
64                marker_options: {
65                    startIconUrl: null,
66                    endIconUrl: null,
67                    shadowUrl: null,
68                },
69                polyline_options: {
70                    color: '#EE4056',
71                    opacity: 0.75,
72                    weight: 5,
73                    lineCap: 'round'
74                },
75            },
76        });
77        elevation.loadGPX(map, gpx_url);
78        // var ele_container = elevation.addTo(map);
79    };
80
81    var load_gpx = function load_gpx(gpx_url) {
82        /*
83          Load the gpx from the given url, add it to the map and feed it to the
84          elevation chart
85        */
86        var gpx = new L.GPX(gpx_url, {
87            async: true,
88            marker_options: {
89                startIconUrl: start_icon,
90                endIconUrl: end_icon,
91                shadowUrl: shadow,
92            },
93            polyline_options: {
94                color: '#EE4056',
95                opacity: 0.75,
96                weight: 5,
97                lineCap: 'round'
98            },
99        });
100
101        gpx.on('loaded', function(e) {
102            map.fitBounds(e.target.getBounds());
103        });
104
105        if (elevation) {
106            gpx.on("addline",function(e){
107                elevation.addData(e.line);
108            });
109        };
110
111        gpx.addTo(map);
112    };
113
114    var render = function render() {
115        // create the map, add elevation, load gpx (only if needed, as the
116        // elevation plugin already loads the gpx data)
117        create_map(latitude, longitude, zoom);
118        if (elevation) {
119            add_elevation_chart();
120        }
121        else {
122            load_gpx(gpx_url);
123        }
124    };
125
126    var that = {}
127    that.render = render;
128    return that
129
130};
131
132
133owjs.week_chart = function(spec) {
134
135    "use strict";
136
137    // parameters provided when creating an "instance" of the chart
138    var chart_selector = spec.chart_selector,
139        url = spec.url,
140        current_day_name = spec.current_day_name
141
142    // Helpers
143    function select_x_axis_label(d) {
144        /* Given a value, return the label associated with it */
145        return d3.select('.x-axis')
146            .selectAll('text')
147            .filter(function(x) { return x == d.name; });
148    }
149
150    // Methods
151    var render = function render() {
152        /*
153           Build a d3 bar chart, populated with data from the given url.
154         */
155        var chart = d3.select(chart_selector),
156            margin = {top: 17, right: 0, bottom: 20, left: 0},
157
158            width = +chart.attr("width") - margin.left - margin.right,
159            height = +chart.attr("height") - margin.top - margin.bottom,
160            g = chart.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"),
161            x = d3.scaleBand().rangeRound([0, width]).padding(0.1),
162            y = d3.scaleLinear().rangeRound([height, 0]);
163
164        d3.json(url, {credentials: "same-origin"}).then(function (data) {
165            x.domain(data.map(function (d) {
166                return d.name;
167            }));
168
169            y.domain([0, d3.max(data, function (d) {
170                return Number(d.distance);
171            })]);
172
173            g.append("g")
174                .attr('class', 'x-axis')
175                .attr("transform", "translate(0," + height + ")")
176                .call(d3.axisBottom(x))
177
178            g.selectAll(".bar")
179                .data(data)
180                .enter().append("rect")
181                .attr("class", function(d) {
182                    if (d.name == current_day_name){
183                        select_x_axis_label(d).attr('style', "font-weight: bold;");
184                        return 'bar current'
185                    }
186                    else {
187                        return 'bar'
188                    }
189                })
190                .attr("x", function (d) {
191                    return x(d.name);
192                })
193                .attr("y", function (d) {
194                    return y(Number(d.distance));
195                })
196                .attr("width", x.bandwidth())
197                .attr("height", function (d) {
198                    return height - y(Number(d.distance));
199                })
200                .on('mouseover', function(d) {
201                    if (d.name != current_day_name){
202                        select_x_axis_label(d).attr('style', "font-weight: bold;");
203                    }
204                })
205                .on('mouseout', function(d) {
206                    if (d.name != current_day_name){
207                        select_x_axis_label(d).attr('style', "font-weight: regular;");
208                    }
209                });
210
211            g.selectAll(".text")
212                .data(data)
213                .enter()
214                .append("text")
215                .attr("class","label")
216                .attr("x", function (d) {
217                    return x(d.name) + x.bandwidth()/2;
218                })
219                .attr("y", function (d) {
220                    /*
221                      Get the value for the current bar, then get the maximum
222                      value to be displayed in the bar, which is used to
223                      calculate the proper position of the label for this bar,
224                      relatively to its height (1% above the bar)
225                     */
226                    var max = y.domain()[1];
227                    return y(d.distance + y.domain()[1] * 0.02);
228            })
229                .text(function(d) {
230                    if (Number(d.distance) > 0) {
231                        return d.distance;
232                    }
233                });
234
235        });
236    };
237
238    var that = {}
239    that.render = render;
240    return that
241
242};
243
244
245owjs.year_chart = function(spec) {
246
247    "use strict";
248
249    // parameters provided when creating an "instance" of the chart
250    var chart_selector = spec.chart_selector,
251        filters_selector = spec.filters_selector,
252        switcher_selector = spec.switcher_selector,
253        is_active_class = spec.is_active_class,
254        is_active_selector = '.' + is_active_class,
255        urls = spec.urls,
256        current_month = spec.current_month,
257        current_week = spec.current_week,
258        y_axis_labels = spec.y_axis_labels,
259        filter_by = spec.filter_by,
260        url = spec.url;
261
262    // Helpers
263    function select_x_axis_label(d) {
264        /* Given a value, return the label associated with it */
265        return d3.select('.x-axis-b')
266            .selectAll('text')
267            .filter(function(x) { return x == d.name; });
268    };
269
270    function get_y_value(d, filter_by) {
271        return Number(d[filter_by]);
272    };
273
274    function get_y_axis_label(filter_by) {
275        return y_axis_labels[filter_by];
276    };
277
278    function get_name_for_x(d) {
279        if (d.week == undefined || d.week == 0) {
280            // Monthly chart or first week of the weekly chart, return
281            // the name of the month, which will be shown in the x-axis
282            // ticks
283            return d.name;
284        }
285        else {
286            // Weekly chart, week other than the first, return the id so
287            // we can place it in the chart, we won't show this to the
288            // user
289            return d.id
290        }
291    }
292
293    // Methods
294    var filters_setup = function filters_setup() {
295        $(filters_selector).on('click', function(e) {
296            e.preventDefault();
297            $(filters_selector + is_active_selector).removeClass(is_active_class);
298            /* $(this).removeClass('is-active'); */
299            filter_by = $(this).attr('class').split('-')[1]
300            $(this).addClass(is_active_class);
301            var chart = d3.select(chart_selector);
302            chart.selectAll("*").remove();
303            render(filter_by, url);
304
305        });
306    };
307
308    var switcher_setup = function switcher_setup() {
309        $(switcher_selector).on('click', function(e) {
310            e.preventDefault();
311            $(switcher_selector + is_active_selector).removeClass(is_active_class);
312            url = $(this).attr('class').split('-')[1]
313            $(this).addClass(is_active_class);
314            var chart = d3.select(chart_selector);
315            chart.selectAll("*").remove();
316            render(filter_by, url);
317        });
318    };
319
320    var render = function render(filter_by, url) {
321        /*
322          Build a d3 bar chart, populated with data from the given url.
323        */
324        var chart = d3.select(chart_selector),
325            margin = {top: 20, right: 20, bottom: 30, left: 50},
326            width = +chart.attr("width") - margin.left - margin.right,
327            height = +chart.attr("height") - margin.top - margin.bottom,
328            g = chart.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"),
329            x = d3.scaleBand().rangeRound([0, width]).padding(0.1),
330            y = d3.scaleLinear().rangeRound([height, 0]);
331
332        d3.json(urls[url], {credentials: "same-origin"}).then(function (data) {
333            x.domain(data.map(function (d) {
334                return get_name_for_x(d);
335                // return d.name;
336            }));
337
338            y.domain([0, d3.max(data, function (d) {
339                return get_y_value(d, filter_by);
340            })]);
341
342            g.append("g")
343                .attr('class', 'x-axis-b')
344                .attr("transform", "translate(0," + height + ")")
345                .call(d3.axisBottom(x))
346
347            g.append("g")
348                .call(d3.axisLeft(y))
349                .append("text")
350                .attr("fill", "#000")
351                .attr("transform", "rotate(-90)")
352                .attr("y", 6)
353                .attr("dy", "0.71em")
354                .attr("text-anchor", "end")
355                .text(get_y_axis_label(filter_by));
356
357            g.selectAll(".bar")
358                .data(data)
359                .enter().append("rect")
360                .attr("class", function(d) {
361                    var sel_week = current_month + '-' + current_week;
362                    if (d.id == current_month || d.id == sel_week){
363                        /* Bar for the currently selected month or week */
364                        select_x_axis_label(d).attr('style', "font-weight: bold;");
365                        return 'bar current';
366                    }
367                    else {
368                        if (!current_week && d.id.indexOf(current_month) >=0 ) {
369                            /*
370                               User selected a month, then switched to weekly
371                               view, we do highlight all the bars for weeks in
372                               that month
373                            */
374                            select_x_axis_label(d).attr('style', "font-weight: bold;");
375                            return 'bar current';
376                        }
377                        else {
378                            /* Non-selected bar */
379                            return 'bar';
380                        }
381
382                    }
383                })
384                .attr("x", function (d) {
385                    return x(get_name_for_x(d));
386                })
387                .attr("y", function (d) {
388                    return y(get_y_value(d, filter_by));
389                })
390                .attr("width", x.bandwidth())
391                .attr("height", function (d) {
392                    return height - y(get_y_value(d, filter_by));
393                })
394                .on('mouseover', function(d) {
395                    if (d.id != current_month){
396                        select_x_axis_label(d).attr('style', "font-weight: bold;");
397                    }
398                })
399                .on('mouseout', function(d) {
400                    if (d.id != current_month){
401                        select_x_axis_label(d).attr('style', "font-weight: regular;");
402                    }
403                })
404                .on('click', function(d) {
405                    window.location.href = d.url;
406                });
407
408            if (url == 'monthly') {
409                g.selectAll(".text")
410                    .data(data)
411                    .enter()
412                    .append("text")
413                    .attr("class","label")
414                    .attr("x", function (d) {
415                        return x(get_name_for_x(d)) + x.bandwidth()/2;
416                    })
417                    .attr("y", function (d) {
418                        /*
419                          Get the value for the current bar, then get the maximum
420                          value to be displayed in the bar, which is used to
421                          calculate the proper position of the label for this bar,
422                          relatively to its height (1% above the bar)
423                        */
424                        var value = get_y_value(d, filter_by);
425                        var max = y.domain()[1];
426                        return y(value + y.domain()[1] * 0.01);
427                    })
428                    .text(function(d) {
429                        var value = get_y_value(d, filter_by)
430                        if ( value > 0) {
431                            return value;
432                        }
433                    });
434            }
435
436            if (url == 'weekly') {
437                g.selectAll(".tick")
438                    .each(function (d, i) {
439                        /*
440                          Remove from the x-axis tickets those without letters
441                          on them (useful for the weekly chart)
442                        */
443                        if (d !== parseInt(d, 10)) {
444                            if(!d.match(/[a-z]/i)) {
445                                this.remove();
446                            }
447                        }
448                    });
449            }
450        });
451    };
452
453    var that = {}
454    that.filters_setup = filters_setup;
455    that.switcher_setup = switcher_setup;
456    that.render = render;
457    return that
458
459};
460
461owjs.calendar_heatmap_chart = function(spec) {
462
463    "use strict"
464
465    var chart_selector = spec.chart_selector,
466        url = spec.url,
467        day_names_list = spec.day_names_list;
468
469    var calendar_rows = function(month) {
470        /*
471          Given a date object that "marks" the beginning of a month
472          return the number of "rows" we need to show all days in that
473          month
474        */
475        // m will be actually the last day of the previous month
476        var m = d3.timeMonth.floor(month);
477        return d3.timeWeeks(d3.timeWeek.floor(m),
478                            d3.timeMonth.offset(m,1)).length;
479    }
480
481    // defaults for the squares/"cells" we will show with the info
482    var cell_margin = 3,
483        cell_size = 25;
484
485
486    var render = function render() {
487        /*
488          Method to actually perform all the loading and rendering of
489          the chart
490        */
491        var chart = d3.select(chart_selector);
492
493        d3.json(url, {credentials: "same-origin"}).then(function (data) {
494
495            if (data.length == 0){
496                // No data for this month, go on with an empty month.
497                var url_year = url.match(/year=([^&]+)/);
498                var url_month = url.match(/month=([^&]+)/);
499                if (url_year != null && url_month != null) {
500                    // if year/month has been passed in the url to retrieve
501                    // workout data, use that to build the current month range.
502                    //
503                    // url_month is 1-12, while js Date() expects 0-11, so we use
504                    // the currently selected month number as the next month and we
505                    // use the previous number as the min date (start of the month)
506                    var min_date = new Date(url_year[1], url_month[1] - 1);
507                    var max_date = new Date(url_year[1], url_month[1])
508                }
509                else {
510                    // otherwise, get the current month range
511                    var min_date = new Date();
512                    var max_date = new Date();
513                    max_date.setDate(min_date.getDate() + 1);
514                }
515            }
516            else {
517                // We have got some workout data, build the min/max dates
518                // from the workouts data, so we can build the proper month
519                // range
520                var min_date = d3.min(data, function(d) {
521                    return new Date(d.day);
522                });
523
524                var max_date = d3.max(data, function(d) {
525                    return new Date(d.day);
526                });
527            }
528
529            // sunday-starting week:
530            // day = d3.timeFormat("%w")
531            // week = d3.timeFormat("%W")
532            // monday-starting week:
533            // day = function(d) {return d3.timeFormat("%u")(d) - 1}
534            // week = d3.timeFormat("%W")
535            var day = function(d) {return d3.timeFormat("%u")(d) - 1},
536                week = d3.timeFormat("%W"),
537                format = d3.timeFormat("%Y-%m-%d"),
538                titleFormat = d3.utcFormat("%a, %d %b"),
539                monthName = d3.timeFormat("%B / %Y"),
540                months = d3.timeMonth.range(d3.timeMonth.floor(min_date), max_date),
541                rows = calendar_rows(max_date);
542
543            // Build the svg image where the chart will be
544            var svg = chart.selectAll("svg")
545                .data(months)
546                .enter().append("svg")
547                .attr("class", "month")
548                .attr("width", (cell_size * 7) + (cell_margin * 8))
549                .attr("height", function(d) {
550                    // we add 50 extra so the month/year label fits
551                    return (cell_size * rows) + (cell_margin * (rows + 1)) + 50;
552                })
553                .append("g");
554
555            // This adds the month/year label above the chart
556            svg.append("text")
557                .attr("class", "month-name")
558                .attr("x", ((cell_size * 7) + (cell_margin * 8)) / 2 )
559                .attr("y", 15)
560                .attr("text-anchor", "middle")
561                .text(function(d) { return monthName(d); });
562
563            // Now, go through each day and add a square/cell for them
564            var rect = svg.selectAll("rect.day")
565                .data(function(d, i) {
566                    return d3.timeDays(
567                        d, new Date(d.getFullYear(), d.getMonth()+1, 1));
568                })
569                .enter().append("rect")
570                .attr("class", "day")
571                .attr("width", cell_size)
572                .attr("height", cell_size)
573                .attr("rx", 6).attr("ry", 6) // rounded corners
574                .attr("fill", '#eaeaea') // default light grey fill
575                .attr("x", function(d) {
576                    return (day(d) * cell_size) + (day(d) * cell_margin) + cell_margin;
577                })
578                .attr("y", function(d) {
579                    var base_value = (week(d) - week(new Date(d.getFullYear(), d.getMonth(), 1)));
580                    return (base_value * cell_size) + (base_value * cell_margin) + cell_margin + 20;
581                })
582                .on("mouseover", function(d) {
583                    d3.select(this).classed('hover', true);
584                })
585                .on("mouseout", function(d) {
586                    d3.select(this).classed('hover', false);
587                })
588                .datum(format);
589
590            rect.append("title")
591                .text(function(d) {return titleFormat(new Date(d));});
592
593            // Add the row with the names of the days
594            var day_names = svg.selectAll('.day-name')
595                .data(day_names_list)
596                .enter().append("text")
597                .attr("class", "day-name")
598                .attr("width", cell_size)
599                .attr("height", cell_size)
600                .attr("x", function(d) {
601                    return (day_names_list.indexOf(d) * cell_size) + (day_names_list.indexOf(d) * cell_margin) + cell_margin * 2;
602                })
603                .attr("y", function(d) {
604                    return ((cell_size * rows) + (cell_margin * (rows + 1))) + 40;
605                })
606                .text(function(d) {
607                    return d;
608                });
609
610            var find_day = function(day) {
611                var found = data.find(function(d) {
612                    return d.day == day;
613                });
614                return found;
615            }
616
617            var lookup = d3.nest()
618                .key(function(d) {
619                    return d.day;
620                })
621                .rollup(function(leaves) {
622                    return leaves[0].time;
623                })
624                .object(data);
625
626            var count = d3.nest()
627                .key(function(d) {
628                    return d.day;
629                })
630                .rollup(function(leaves) {
631                    return leaves[0].time;
632                })
633                .entries(data);
634
635            var scale = d3.scaleLinear()
636                .domain(d3.extent(count, function(d) {
637                    return d.value;
638                }))
639                .range(['#f8b5be', '#f60002']);
640
641            rect.filter(function(d) {
642                return d in lookup;
643            })
644                .style("fill", function(d) {
645                    // Fill in with the proper color
646                    return scale([lookup[d]]);
647                })
648                .classed("clickable", true)
649                .on("click", function(d){
650                    if(d3.select(this).classed('focus')){
651                        d3.select(this).classed('focus', false);
652                    } else {
653                        d3.select(this).classed('focus', true)
654                    }
655                    // doSomething();
656                })
657                .select("title")
658                .text(function(d) {
659                    // Update the title adding some more info
660                    var day = find_day(d);
661                    return titleFormat(new Date(d)) + ":  " + day.time_formatted; });
662        });
663
664    };
665
666    var that = {}
667    that.render = render;
668    return that
669
670};
671
672
673owjs.map_shots = function(spec) {
674
675    "use strict";
676
677    var img_selector = spec.img_selector;
678
679    var run = function run(){
680        $(img_selector).each(function(){
681            var img = $(this);
682            var a = $(this).parent();
683            var url = a.attr('href') + 'map-shot';
684            var jqxhr = $.getJSON(url, function(info) {
685                img.fadeOut('fast', function () {
686                    img.attr('src', info['url']);
687                    img.fadeIn('fast');
688                });
689                img.removeClass('js-needs-map');
690            });
691        });
692    };
693
694    var that = {}
695    that.run = run;
696    return that
697
698};
699
700
701owjs.sport_stats = function(spec) {
702
703    "use strict";
704
705    var link_selector = spec.link_selector;
706    var stats_selector = spec.stats_selector;
707    var selected = spec.selected;
708    var dropdown_selector = spec.dropdown_selector;
709    var current_year = spec.current_year;
710    var year_link_selector = spec.year_link_selector;
711
712    var setup = function setup() {
713        // Hide all sports stats by default
714        $(stats_selector).hide();
715        // Show the pre-selected one
716        $(selected).show();
717
718        $(link_selector).on('click', function(e) {
719            e.preventDefault();
720            var selected = $(this).attr('class').split(' ')[1];
721            var sport = selected.split('-')[1]
722            // Hide them all
723            $(stats_selector).hide();
724            // Show the selected one
725            $(stats_selector + '.' + selected).show();
726            // Update the sport on the sports selector widget
727            $(dropdown_selector + ' strong').html(sport);
728            // finally "click" on the proper year to be displayed for this sport
729            $(year_link_selector + sport + '-' + current_year).click();
730        });
731    };
732
733    var that = {}
734    that.setup = setup;
735    return that
736
737};
738
739
740owjs.year_stats = function(spec) {
741
742    "use strict";
743
744    var link_selector = spec.link_selector;
745    var stats_selector = spec.stats_selector;
746    var selected = spec.selected;
747    var dropdown_selector = spec.dropdown_selector;
748
749    var setup = function setup() {
750        // Hide all years stats by default
751        $(stats_selector).hide();
752        // Show the pre-selected one
753        $(selected).show();
754
755        $(link_selector).on('click', function(e) {
756            e.preventDefault();
757            var selected = $(this).attr('class').split(' ')[1];
758            var sport = selected.split('-')[1]
759            var year = selected.split('-')[2]
760            // Hide them all
761            $(stats_selector).hide();
762            // Show the selected one
763            $(stats_selector + '.' + selected).show();
764            // Update the year on the years selector widget
765            $(dropdown_selector + sport + ' strong').html(year);
766        });
767    };
768
769    var that = {}
770    that.setup = setup;
771    return that
772
773};
Note: See TracBrowser for help on using the repository browser.