source: OpenWorkouts-current/ow/static/js/ow.js @ 39dc0a6

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

(#7) Pass in the selector name for the calendar heatmap tooltip container
when building an "instance" of the calendar chart code.

(#7) Fixed a bug in the fade in/out of the heatmap tooltip (tooltip display
were queued so the tooltip keep fading in and out for a while after the
user did stop moving around on the chart)

  • Property mode set to 100644
File size: 26.2 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        tooltip_selector = spec.tooltip_selector,
467        url = spec.url,
468        day_names_list = spec.day_names_list;
469
470    var calendar_rows = function(month) {
471        /*
472          Given a date object that "marks" the beginning of a month
473          return the number of "rows" we need to show all days in that
474          month
475        */
476        // m will be actually the last day of the previous month
477        var m = d3.timeMonth.floor(month);
478        return d3.timeWeeks(d3.timeWeek.floor(m),
479                            d3.timeMonth.offset(m,1)).length;
480    }
481
482    // defaults for the squares/"cells" we will show with the info
483    var cell_margin = 3,
484        cell_size = 25;
485
486
487    var render = function render() {
488        /*
489          Method to actually perform all the loading and rendering of
490          the chart
491        */
492
493        // Hide the tooltip
494        $(tooltip_selector).hide()
495
496        var chart = d3.select(chart_selector);
497
498        d3.json(url, {credentials: "same-origin"}).then(function (data) {
499
500            if (data.length == 0){
501                // No data for this month, go on with an empty month.
502                var url_year = url.match(/year=([^&]+)/);
503                var url_month = url.match(/month=([^&]+)/);
504                if (url_year != null && url_month != null) {
505                    // if year/month has been passed in the url to retrieve
506                    // workout data, use that to build the current month range.
507                    //
508                    // url_month is 1-12, while js Date() expects 0-11, so we use
509                    // the currently selected month number as the next month and we
510                    // use the previous number as the min date (start of the month)
511                    var min_date = new Date(url_year[1], url_month[1] - 1);
512                    var max_date = new Date(url_year[1], url_month[1])
513                }
514                else {
515                    // otherwise, get the current month range
516                    var min_date = new Date();
517                    var max_date = new Date();
518                    max_date.setDate(min_date.getDate() + 1);
519                }
520            }
521            else {
522                // We have got some workout data, build the min/max dates
523                // from the workouts data, so we can build the proper month
524                // range
525                var min_date = d3.min(data, function(d) {
526                    return new Date(d.day);
527                });
528
529                var max_date = d3.max(data, function(d) {
530                    return new Date(d.day);
531                });
532            }
533
534            // sunday-starting week:
535            // day = d3.timeFormat("%w")
536            // week = d3.timeFormat("%W")
537            // monday-starting week:
538            // day = function(d) {return d3.timeFormat("%u")(d) - 1}
539            // week = d3.timeFormat("%W")
540            var day = function(d) {return d3.timeFormat("%u")(d) - 1},
541                week = d3.timeFormat("%W"),
542                format = d3.timeFormat("%Y-%m-%d"),
543                titleFormat = d3.utcFormat("%a, %d %b"),
544                monthName = d3.timeFormat("%B / %Y"),
545                months = d3.timeMonth.range(d3.timeMonth.floor(min_date), max_date),
546                rows = calendar_rows(max_date);
547
548            var find_day = function(day) {
549                var found = data.find(function(d) {
550                    return d.day == day;
551                });
552                return found;
553            }
554
555            // Build the svg image where the chart will be
556            var svg = chart.selectAll("svg")
557                .data(months)
558                .enter().append("svg")
559                .attr("class", "month")
560                .attr("width", (cell_size * 7) + (cell_margin * 8))
561                .attr("height", function(d) {
562                    // we add 50 extra so the month/year label fits
563                    return (cell_size * rows) + (cell_margin * (rows + 1)) + 50;
564                })
565                .append("g");
566
567            // This adds the month/year label above the chart
568            svg.append("text")
569                .attr("class", "month-name")
570                .attr("x", ((cell_size * 7) + (cell_margin * 8)) / 2 )
571                .attr("y", 15)
572                .attr("text-anchor", "middle")
573                .text(function(d) { return monthName(d); });
574
575            // Now, go through each day and add a square/cell for them
576            var rect = svg.selectAll("rect.day")
577                .data(function(d, i) {
578                    return d3.timeDays(
579                        d, new Date(d.getFullYear(), d.getMonth()+1, 1));
580                })
581                .enter().append("rect")
582                .attr("class", "day")
583                .attr("width", cell_size)
584                .attr("height", cell_size)
585                .attr("rx", 6).attr("ry", 6) // rounded corners
586                .attr("fill", '#eaeaea') // default light grey fill
587                .attr("x", function(d) {
588                    return (day(d) * cell_size) + (day(d) * cell_margin) + cell_margin;
589                })
590                .attr("y", function(d) {
591                    var base_value = (week(d) - week(new Date(d.getFullYear(), d.getMonth(), 1)));
592                    return (base_value * cell_size) + (base_value * cell_margin) + cell_margin + 20;
593                })
594                .on("mouseover", function(d) {
595                    d3.select(this).classed('hover', true);
596                    var day = find_day(d);
597                    $(tooltip_selector + '-date').html(titleFormat(new Date(d)));
598                    if (day != undefined) {
599                        $(tooltip_selector + '-workouts').html(day.workouts);
600                        $(tooltip_selector + '-time').html(day.time_formatted);
601                        $(tooltip_selector + '-distance').html(day.distance_formatted);
602                        $(tooltip_selector + '-elevation').html(day.elevation_formatted);
603                    }
604                    else {
605                        $(tooltip_selector + '-workouts').html(' - ');
606                        $(tooltip_selector + '-time').html(' - ');
607                        $(tooltip_selector + '-distance').html(' - ');
608                        $(tooltip_selector + '-elevation').html(' - ');
609                    }
610                    $(tooltip_selector).stop( true, true ).fadeIn('fast');
611                })
612                .on("mouseout", function(d) {
613                    d3.select(this).classed('hover', false);
614                    $(tooltip_selector).stop( true, true ).fadeOut('fast');
615                })
616                .datum(format);
617
618            rect.append("title")
619                .text(function(d) {return titleFormat(new Date(d));});
620
621            // Add the row with the names of the days
622            var day_names = svg.selectAll('.day-name')
623                .data(day_names_list)
624                .enter().append("text")
625                .attr("class", "day-name")
626                .attr("width", cell_size)
627                .attr("height", cell_size)
628                .attr("x", function(d) {
629                    return (day_names_list.indexOf(d) * cell_size) + (day_names_list.indexOf(d) * cell_margin) + cell_margin * 2;
630                })
631                .attr("y", function(d) {
632                    return ((cell_size * rows) + (cell_margin * (rows + 1))) + 40;
633                })
634                .text(function(d) {
635                    return d;
636                });
637
638            var lookup = d3.nest()
639                .key(function(d) {
640                    return d.day;
641                })
642                .rollup(function(leaves) {
643                    return leaves[0].time;
644                })
645                .object(data);
646
647            var count = d3.nest()
648                .key(function(d) {
649                    return d.day;
650                })
651                .rollup(function(leaves) {
652                    return leaves[0].time;
653                })
654                .entries(data);
655
656            var scale = d3.scaleLinear()
657                .domain(d3.extent(count, function(d) {
658                    return d.value;
659                }))
660                .range(['#f8b5be', '#f60002']);
661
662            rect.filter(function(d) {
663                return d in lookup;
664            })
665                .style("fill", function(d) {
666                    // Fill in with the proper color
667                    return scale([lookup[d]]);
668                })
669                .classed("clickable", true)
670                .on("click", function(d){
671                    if(d3.select(this).classed('focus')){
672                        d3.select(this).classed('focus', false);
673                    } else {
674                        d3.select(this).classed('focus', true)
675                    }
676                    // doSomething();
677                })
678        });
679
680    };
681
682    var that = {}
683    that.render = render;
684    return that
685
686};
687
688
689owjs.map_shots = function(spec) {
690
691    "use strict";
692
693    var img_selector = spec.img_selector;
694
695    var run = function run(){
696        $(img_selector).each(function(){
697            var img = $(this);
698            var a = $(this).parent();
699            var url = a.attr('href') + 'map-shot';
700            var jqxhr = $.getJSON(url, function(info) {
701                img.fadeOut('fast', function () {
702                    img.attr('src', info['url']);
703                    img.fadeIn('fast');
704                });
705                img.removeClass('js-needs-map');
706            });
707        });
708    };
709
710    var that = {}
711    that.run = run;
712    return that
713
714};
715
716
717owjs.sport_stats = function(spec) {
718
719    "use strict";
720
721    var link_selector = spec.link_selector;
722    var stats_selector = spec.stats_selector;
723    var selected = spec.selected;
724    var dropdown_selector = spec.dropdown_selector;
725    var current_year = spec.current_year;
726    var year_link_selector = spec.year_link_selector;
727
728    var setup = function setup() {
729        // Hide all sports stats by default
730        $(stats_selector).hide();
731        // Show the pre-selected one
732        $(selected).show();
733
734        $(link_selector).on('click', function(e) {
735            e.preventDefault();
736            var selected = $(this).attr('class').split(' ')[1];
737            var sport = selected.split('-')[1]
738            // Hide them all
739            $(stats_selector).hide();
740            // Show the selected one
741            $(stats_selector + '.' + selected).show();
742            // Update the sport on the sports selector widget
743            $(dropdown_selector + ' strong').html(sport);
744            // finally "click" on the proper year to be displayed for this sport
745            $(year_link_selector + sport + '-' + current_year).click();
746        });
747    };
748
749    var that = {}
750    that.setup = setup;
751    return that
752
753};
754
755
756owjs.year_stats = function(spec) {
757
758    "use strict";
759
760    var link_selector = spec.link_selector;
761    var stats_selector = spec.stats_selector;
762    var selected = spec.selected;
763    var dropdown_selector = spec.dropdown_selector;
764
765    var setup = function setup() {
766        // Hide all years stats by default
767        $(stats_selector).hide();
768        // Show the pre-selected one
769        $(selected).show();
770
771        $(link_selector).on('click', function(e) {
772            e.preventDefault();
773            var selected = $(this).attr('class').split(' ')[1];
774            var sport = selected.split('-')[1]
775            var year = selected.split('-')[2]
776            // Hide them all
777            $(stats_selector).hide();
778            // Show the selected one
779            $(stats_selector + '.' + selected).show();
780            // Update the year on the years selector widget
781            $(dropdown_selector + sport + ' strong').html(year);
782        });
783    };
784
785    var that = {}
786    that.setup = setup;
787    return that
788
789};
Note: See TracBrowser for help on using the repository browser.