Skip to content
Extraits de code Groupes Projets
visitor-map.js 67 ko
Newer Older
  • Learn to ignore specific revisions
  • mattab's avatar
    mattab a validé
    /*!
     * Piwik - Web Analytics
     *
     * Visitors Map with zoom in continents / countries. Cities + Region view.
     * Using Kartograph.js http://kartograph.org/
     *
     * @link http://piwik.org
     * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
     */
    
    
        // create a global namespace for UserCountryMap plugin
        // this is used both by visitor map and realtime map
        window.UserCountryMap = window.UserCountryMap || {};
    
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
        // the main class for this widget, provides the interface for the template
    
        var VisitorMap = window.UserCountryMap.VisitorMap = function (config, theWidget) {
    
            this.config = config;
    
            this.theWidget = theWidget || false;
    
            this.run();
        };
    
        $.extend(VisitorMap.prototype, {
    
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
            /*
             * initializes the map after widget creation
             */
    
                var self = this,
                    config = self.config;
    
                /*
                 * our own custom selector to only select stuff of this widget
                 */
                function $$(selector) {
    
                    return $(selector, self.theWidget ? self.theWidget.element : undefined);
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                }
    
    
                var mapContainer = $$('.UserCountryMap_map').get(0),
                    map = self.map = Kartograph.map(mapContainer),
                    main = $$('.UserCountryMap_container'),
                    worldTotalVisits = 0,
                    width = main.width(),
                    _ = config._;
    
                config.noDataColor = '#E4E2D7';
                self.widget = $$('.widgetUserCountryMapvisitorMap').parent();
    
                //window.__mapInstances = window.__mapInstances || [];
                //window.__mapInstances.push(map);
    
                function _reportParams(module, action, countryFilter) {
                    var params = $.extend(config.reqParams, {
                        module: 'API',
                        method: 'API.getProcessedReport',
                        apiModule: module,
                        apiAction: action,
                        filter_limit: -1,
                        limit: -1
                    });
                    if (countryFilter) {
                        $.extend(params, {
                            filter_column: 'country',
                            filter_sort_column: 'nb_visits',
                            filter_pattern: countryFilter
                        });
                    }
                    return params;
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                }
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                /*
                 * wrapper around jQuery.ajax, moves token_auth parameter
                 * to POST data while keeping other parameters as GET
                 */
                function ajax(params, dataType) {
                    dataType = dataType || 'json';
                    params = $.extend({}, params);
    
                    var token_auth = '' + params.token_auth;
    
                    delete params['token_auth'];
                    return $.ajax({
                        url: 'index.php?' + $.param(params),
                        dataType: dataType,
                        data: { token_auth: token_auth },
                        type: 'POST'
                    });
                }
    
                function minmax(values) {
    
                    values = values.sort(function (a, b) { return Number(a) - Number(b); });
    
                    return {
                        min: values[0],
    
                        max: values[values.length - 1],
                        median: values[Math.floor(values.length * 0.5)],
                        p33: values[Math.floor(values.length * 0.33)],
                        p66: values[Math.floor(values.length * 0.66)],
                        p90: values[Math.floor(values.length * 0.9)]
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                }
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                function formatNumber(v) {
                    v = Number(v);
    
                    return v > 1000000 ? (v / 1000000).toFixed(1) + 'm' :
                        v > 1000 ? (v / 1000).toFixed(1) + 'k' :
                            v;
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                //
                // Since some metrics are transmitted in an non-numeric format like
                // "61.45%", we need to parse the numbers to make sure they can be
                // used for color scales etc. The parsed metrics will be stored as
                // METRIC_raw
                //
                function formatValueForTooltips(data, metric, id) {
    
    
                    var val = data[metric] % 1 === 0 || Number(data[metric]) != data[metric] ? data[metric] : data[metric].toFixed(1),
    
                        v = _[metric].replace('%s', '<strong>' + val + '</strong>');
    
    mattab's avatar
    mattab a validé
                    if (val == 1 && metric == 'nb_visits') v = _.one_visit;
    
    
                    function avgTime(d) { return d['sum_visit_length'] / d['nb_visits']; }
    
                    if (metric.substr(0, 3) == 'nb_' && metric != 'nb_actions_per_visit') {
                        var total;
                        if (id.length == 3) total = UserCountryMap.countriesByIso[id][metric];
                        else if (id == 'world') total = _worldTotal;
                        else {
                            total = 0;
    
                            $.each(UserCountryMap.countriesByIso, function (iso, country) {
    
                                if (UserCountryMap.ISO3toCONT[iso] == id) {
                                    total += country[metric];
                                }
                            });
                        }
                        if (total) {
    
                            v += ' (' + formatPercentage(data[metric] / total) + ')';
    
                        }
                    } else if (metric == 'avg_time_on_site') {
    
                        v += '<br/> (' + _.nb_visits.replace('%s', data.nb_visits) + ')';
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                }
    
    
                function getColorScale(rows, metric, filter, choropleth) {
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
    
    
                    var colscale;
    
                    function addLegendItem(val, first) {
                        var d = $('<div>'), r = $('<div>'), l = $('<div>'),
                            metric = $$('.userCountryMapSelectMetrics').val(),
                            v = formatNumber(Math.round(val)) + (metric == 'avg_time_on_site' ? first ? ' sec' : 's' : '');
    
                        d.css({ width: 17, height: 17, float: 'left', background: colscale(val) });
    
                        l.css({ 'margin-left': 20, 'line-height': '20px', 'text-align': 'right' }).html(v);
    
                        r.css({ clear: 'both', height: 19 });
                        r.append(d).append(l);
                        $('.UserCountryMap-legend .content').append(r);
                    }
    
                    var stats, values = [], id = self.lastSelected, c;
    
    
                        if (!$.isFunction(filter) || filter(r)) {
                            var v = quantify(r, metric);
                            if (!isNaN(v)) values.push(v);
                        }
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                    });
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                    stats = minmax(values);
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                    if (stats.min == stats.max) {
    
                        colscale = function () { return chroma.hex('#CDDAEF'); };
    
                        if (choropleth) {
                            $('.UserCountryMap-legend .content').html('').show();
                            addLegendItem(stats.min, true);
                        }
                        return colscale;
                    }
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                    colscale = chroma.scale()
                        .range([choropleth ? '#CDDAEF' : '#385993', '#385993'])
                        .domain(values, 4, 'c')
                        .mode('lch');
    
                    if (metric == 'avg_time_on_site' || metric == 'nb_actions_per_visit' || metric == 'bounce_rate') {
                        if (id.length == 3) {
                            c = (stats.p90 - stats.min) / (stats.max - stats.min);
    
                            colscale = chroma.scale(['#385993', '#385993', '#E87500', '#E87500'], [0, c, c + 0.001, 1])
    
                                .domain(chroma.limits(rows, 'c', 5, 'curMetric', filter))
                                .mode('hsl');
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                        }
                    }
    
    
                    // a good place to update the legend, isn't it?
                    if (choropleth) {
                        $('.UserCountryMap-legend .content').html('').show();
                        var itemExists = {};
    
                        $.each(chroma.limits(values, 'k', 3), function (i, v) {
    
                            if (itemExists[v]) return;
                            addLegendItem(v, i === 0);
                            itemExists[v] = true;
                        });
    
                    } else {
                        $('.UserCountryMap-legend .content').hide();
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                    }
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
    
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                function formatPercentage(val) {
                    if (val < 0.001) return '< 0.1%';
    
                    return Math.round(1000 * val) / 10 + '%';
    
                /*
                 * to ensure that onResize is not called a hundred times
                 * while resizing the browser window, this functions
                 * makes sure to only call onResize at the end
                 */
                function onResizeLazy() {
    
                    clearTimeout(self._resizeTimer);
    
                    self._resizeTimer = setTimeout(self.resize.bind(self), 300);
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                }
    
                function activateButton(btn) {
                    $$('.UserCountryMap-view-mode-buttons a').removeClass('activeIcon');
                    btn.addClass('activeIcon');
                    $$('.UserCountryMap-activeItem').offset({ left: btn.offset().left });
                }
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
    
    
                function initUserInterface() {
                    // react to changes of country select
    
                    $$('.userCountryMapSelectCountry').off('change').change(function () {
    
                        updateState($$('.userCountryMapSelectCountry').val());
                    });
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
    
    
                    function zoomOut() {
                        var t = self.lastSelected,
                            tgt = 'world';  // zoom out to world per default..
                        if (t.length == 3 && UserCountryMap.ISO3toCONT[t] !== undefined) {
                            tgt = UserCountryMap.ISO3toCONT[t];  // ..but zoom to continent if we know it
                        }
                        updateState(tgt);
                    }
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
    
    
                    // enable zoom-out
                    $$('.UserCountryMap-btn-zoom').off('click').click(zoomOut);
                    $$('.UserCountryMap_map').off('click').click(zoomOut);
    
                    // handle window resizes
                    $(window).off('resize').resize(onResizeLazy);
    
    
    mattab's avatar
    mattab a validé
                    // enable metric changes
    
                    $$('.userCountryMapSelectMetrics').off('change').change(function () {
    
                        updateState(self.lastSelected);
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                    });
    
    
                    // handle city button
    
                    (function (btn) {
                        btn.off('click').click(function () {
    
                            if (self.lastSelected.length == 3) {
                                if (self.mode != "city") {
                                    self.mode = "city";
                                    updateState(self.lastSelected);
                                }
                            }
                        });
                    })($$('.UserCountryMap-btn-city'));
    
                    // handle region button
    
                    (function (btn) {
                        btn.off('click').click(function () {
    
                            if (self.mode != "region") {
                                $$('.UserCountryMap-view-mode-buttons a').removeClass('activeIcon');
                                self.mode = "region";
                                updateState(self.lastSelected);
                            }
                        });
                    })($$('.UserCountryMap-btn-region'));
    
                    // add loading indicator overlay
    
    mattab's avatar
    mattab a validé
    
    
                    var bl = $('<div id="UserCountryMap-black"></div>');
                    bl.hide();
                    $$('.UserCountryMap_map').append(bl);
    
                    var infobtn = $('.UserCountryMap-info-btn');
    
                    infobtn.off('mouseenter').on('mouseenter',function (e) {
    
                        $(infobtn.data('tooltip-target')).show();
    
                    }).off('mouseleave').on('mouseleave', function (e) {
                            $(infobtn.data('tooltip-target')).hide();
                        });
    
                    $('.UserCountryMap-tooltip').hide();
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                }
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
    
    
                /*
                 * updateState, called whenever the view changes
                 */
                function updateState(id) {
                    // double check view mode
                    if (self.mode == "city" && id.length != 3) {
                        // city mode is reserved for country views
                        self.mode = "region";
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
                    }
    
    
                    var metric = $$('.userCountryMapSelectMetrics').val();
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                    // store current map state
                    self.widget.dashboardWidget('setParameters', {
                        lastMap: id, viewMode: self.mode, lastMetric: metric
                    });
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
    
    
                    $('.UserCountryMap-info-btn').hide();
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
    
    
                    try {
                        // check which map to render
                        if (id.length == 3) {
                            // country map
                            renderCountryMap(id, metric);
                        } else {
                            // world or continent map
                            renderWorldMap(id, metric);
                        }
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
    
    
                    } catch (e) {
                        // console.error(e);
                        $('.UserCountryMap-info .content').html(e);
                        $('.UserCountryMap-info').show();
    
    
                    _updateUI(id, metric);
    
                    self.lastSelected = id;
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                }
    
    
                /*
                 * update the widgets ui according to the currently selected view
                 */
                function _updateUI(id, metric) {
                    // update UI
                    if (self.mode == "city") {
                        activateButton($$('.UserCountryMap-btn-city'));
                    } else {
                        activateButton($$('.UserCountryMap-btn-region'));
                    }
                    var countrySelect = $$('.userCountryMapSelectCountry');
                    countrySelect.val(id);
    
                    var zoom = $$('.UserCountryMap-btn-zoom');
                    if (id == 'world') zoom.addClass('inactiveIcon');
                    else zoom.removeClass('inactiveIcon');
    
                    // show flag icon in select box
                    var flag = $$('.userCountryMapFlag'),
                        regionBtn = $$('.UserCountryMap-btn-region');
                    if (id.length == 3) {
                        if (UserCountryMap.countriesByIso[id]) {  // we have visits in this country
                            flag.css({
    
                                'background-image': 'url(' + UserCountryMap.countriesByIso[id].flag + ')',
    
                                'background-repeat': 'no-repeat',
                                'background-position': '5px 5px'
                            });
                            $$('.UserCountryMap-btn-city').removeClass('inactiveIcon').show();
                            $('span', regionBtn).html(regionBtn.data('region'));
                        } else {
                            // not a single visit in this country
                            $$('.UserCountryMap-btn-city').addClass('inactiveIcon');
                            $('.map-stats').html(_.no_data);
                            $('.map-title').html('');
                            return;
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                        }
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
    
    
                    } else {
                        flag.css({
                            'background': 'none'
                        });
                        $$('.UserCountryMap-btn-city').addClass('inactiveIcon').hide();
                        $('span', regionBtn).html(regionBtn.data('country'));
                    }
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
    
    
                    var mapTitle = id.length == 3 ?
                            UserCountryMap.countriesByIso[id].name :
    
                            $$('.userCountryMapSelectCountry option[value=' + id + ']').html(),
    
                        totalVisits = 0;
                    // update map title
                    $('.map-title').html(mapTitle);
    
                    $$('.widgetUserCountryMapvisitorMap .widgetName .map-title').html('' + mapTitle);
    
                    // update total visits for that region
                    if (id.length == 3) {
                        totalVisits = UserCountryMap.countriesByIso[id]['nb_visits'];
                    } else if (id.length == 2) {
    
                        $.each(UserCountryMap.countriesByIso, function (iso, country) {
    
                            if (UserCountryMap.ISO3toCONT[iso] == id) {
                                totalVisits += country['nb_visits'];
                            }
                        });
                    } else {
                        totalVisits = self.config.visitsSummary['nb_visits'];
                    }
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
    
    
                    if (id.length == 3) {
                        $('.map-stats').html(formatValueForTooltips(UserCountryMap.countriesByIso[id], metric, 'world'));
                    } else {
                        $('.map-stats').html(
    
                            _.nb_visits.replace('%s', '<strong>' + formatNumber(totalVisits) + '</strong>') + (id != 'world' ? ' (' +
    
                                formatPercentage(totalVisits / worldTotalVisits) + ')' : '')
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                    }
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
    
    
                /*
                 * called by updateState if either the world or a continent is selected
                 */
                function renderWorldMap(target, metric) {
    
                    /**
                     * update the colors of the countrys
                     */
                    function updateColorsAndTooltips(metric) {
    
                        // Create a chroma ColorScale for the selected metric that regards only the
                        // countries that are visible in the map.
    
                        colscale = getColorScale(UserCountryMap.countryData, metric, function (r) {
    
                            if (target.length == 2) {
                                return UserCountryMap.ISO3toCONT[r.iso] == target;
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                            } else {
    
                                return true;
                            }
                        }, true);
    
                        function countryFill(data) {
                            var d = UserCountryMap.countriesByIso[data.iso];
                            if (d === null) {
                                return self.config.noDataColor;
                            } else {
    
                                return colscale(d[metric]);
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                            }
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
    
    
                        // Apply the color scale to the map.
                        map.getLayer('countries')
    
                            .style('fill', countryFill)
                            .on('mouseenter', function (d, path, evt) {
    
                                if (evt.shiftKey) { // highlight on mouseover with shift pressed
                                    path.attr('fill', '#f4f45b');
                                }
                            })
    
                            .on('mouseleave', function (d, path, evt) {
    
                                if ($.inArray(UserCountryMap.countriesByIso[d.iso].name, _rowEvolution.labels) == -1) {
                                    path.attr('fill', countryFill(d)); // reset color
                                }
                            });
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                        // Update the map tooltips.
    
                        map.getLayer('countries').tooltips(function (data) {
    
                            var metric = $$('.userCountryMapSelectMetrics').val(),
                                country = UserCountryMap.countriesByIso[data.iso];
    
                            return '<h3>' + country.name + '</h3>' +
    
                                formatValueForTooltips(country, metric, target);
                        });
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                    }
    
    
                    // if the view hasn't changed (but probably the selected metric),
                    // all we need to do is to recolor the current map.
                    if (target == self.lastSelected) {
                        updateColorsAndTooltips(metric);
                        return;
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                    }
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                    // otherwise we need to load another map svg
    
                    _updateMap(target + '.svg', function () {
    
    
                        // add a layer for non-selectable countries = for which no data is
                        // defined in the current report
                        map.addLayer('countries', {
                            name: 'context',
    
                                return UserCountryMap.countriesByIso[pd.iso] === undefined;
                            },
    
                            tooltips: function (pd) {
                                return '<h3>' + pd.name + '</h3>' + _.no_visit;
    
    Gregor Aisch's avatar
    ..  
    Gregor Aisch a validé
                        });
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                        // add a layer for selectable countries = for which we have data
                        // available in the current report
    
                        map.addLayer('countries', { name: 'countryBG', filter: function (pd) {
    
                            return UserCountryMap.countriesByIso[pd.iso] !== undefined;
                        }});
    
                        map.addLayer('countries', {
                            key: 'iso',
    
                                return UserCountryMap.countriesByIso[pd.iso] !== undefined;
                            },
    
                            click: function (data, path, evt) {
    
                                evt.stopPropagation();
                                if (evt.shiftKey || _rowEvolution.labels.length) {
                                    if (evt.altKey) {
                                        path.attr('fill', '#f4f45b');
                                        addMultipleRowEvolution('getCountry', UserCountryMap.countriesByIso[data.iso].name);
                                    } else {
                                        showRowEvolution('getCountry', UserCountryMap.countriesByIso[data.iso].name);
                                        updateColorsAndTooltips(metric);
                                    }
                                    return;
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                                }
    
                                var tgt;
                                if (self.lastSelected != 'world' || UserCountryMap.countriesByIso[data.iso] === undefined) {
                                    tgt = data.iso;
                                } else {
                                    tgt = UserCountryMap.ISO3toCONT[data.iso];
                                }
                                updateState(tgt);
                            }
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                        });
    
    
                        updateColorsAndTooltips(metric);
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                    });
    
                }
    
    
                /*
                 * updateMap is called by renderCountryMap() and renderWorldMap()
                 */
                function _updateMap(svgUrl, callback) {
    
                    map.loadMap(config.svgBasePath + svgUrl, function () {
    
    
                        map.clear();
                        self.resize();
                        callback();
    
                        $('.ui-tooltip').remove(); // remove all existing tooltips
    
                    }, { padding: -3});
                }
    
                function indicateLoading() {
                    $$('.UserCountryMap-black').show();
                    $$('.UserCountryMap-black').css('opacity', 0);
                    $$('.UserCountryMap-black').animate({ opacity: 0.5 }, 400);
                    $$('.UserCountryMap .loadingPiwik').show();
                }
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                function loadingComplete() {
                    $$('.UserCountryMap-black').hide();
                    $$('.UserCountryMap .loadingPiwik').hide();
                }
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                /*
                 * returns a quantifiable value for a given metric
                 */
                function quantify(d, metric) {
                    if (!metric) metric = $$('.userCountryMapSelectMetrics').val();
                    switch (metric) {
                        case 'avg_time_on_site':
                            return d.sum_visit_length / d.nb_visits;
                        case 'bounce_rate':
                            return d.bounce_count / d.nb_visits;
                        default:
                            return d[metric];
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                    }
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                /*
                 * Aggregates a list of report rows by a given grouping function
                 *
                 * the groupBy function gets a row as argument add should return a
                 * group-id or false, if the row should be ignored.
                 *
                 * all rows for which groupBy returns the same group-id are
                 * aggregated according to the given metric.
                 */
                function aggregate(rows, groupBy) {
    
                    var groups = {};
    
                    $.each(rows, function (i, row) {
    
                        var g_id = groupBy ? groupBy(row) : 'X';
                        g_id = g_id === true ? $.isNumeric(i) && i === Number(i) ? false : i : g_id;
                        if (g_id) {
                            if (!groups[g_id]) {
                                groups[g_id] = {
                                    nb_visits: 0,
                                    nb_actions: 0,
                                    sum_visit_length: 0,
                                    bounce_count: 0
                                };
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                            }
    
                            $.each(groups[g_id], function (metric) {
    
                                groups[g_id][metric] += row[metric];
                            });
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                        }
                    });
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                    $.each(groups, function (g_id, group) {
    
                        var apv = group.nb_actions / group.nb_visits,
                            ats = group.sum_visit_length / group.nb_visits,
                            br = (group.bounce_count * 100 / group.bounce_count);
                        group['nb_actions_per_visit'] = apv;
    
                        group['avg_time_on_site'] = new Date(0, 0, 0, ats / 3600, ats % 3600 / 60, ats % 60).toLocaleTimeString();
                        group['bounce_rate'] = (br % 1 !== 0 ? br.toFixed(1) : br) + "%";
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                    });
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                    return groupBy ? groups : groups.X;
                }
    
                function displayUnlocatableCount(unlocated, total) {
                    $('.unlocated-stats').html(
                        $('.unlocated-stats').data('tpl')
                            .replace('%s', unlocated)
    
                            .replace('%p', '(' + formatPercentage(unlocated / total) + ')')
    
                            .replace('%c', UserCountryMap.countriesByIso[self.lastSelected].name)
                    );
                    $('.UserCountryMap-info-btn').show();
                }
    
                /*
                 * renders a country map (either region or city view)
                 */
                function renderCountryMap(iso) {
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                    var countryMap = {
                        zoomed: false,
                        lastRequest: false,
                        lastResponse: false
                    };
    
                    /*
                     * updates the colors in the current region map
                     * this happens once a new country is loaded and
                     * whenever the metric changes
                     */
                    function updateRegionColors() {
                        indicateLoading();
                        // load data from Piwik API
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                        ajax(_reportParams('UserCountry', 'getRegion', UserCountryMap.countriesByIso[iso].iso2))
    
                                var regionDict = {},
                                    totalCountryVisits = UserCountryMap.countriesByIso[iso].nb_visits,
                                    unlocated = totalCountryVisits;
                                // self.lastReportMetricStats = {};
    
                                function regionCode(region) {
                                    var key = UserCountryMap.keys[iso] || 'fips';
                                    return key.substr(0, 4) == "fips" ? region[key].substr(2) : region[key];  // cut first two letters from fips code (=country code)
                                }
    
                                function regionExistsInMap(code) {
                                    var key = UserCountryMap.keys[iso] || 'fips', q = {};
                                    q[key] = key.substr(0, 4) == 'fips' ? UserCountryMap.countriesByIso[iso].fips + code : code;
                                    if (map.getLayer('regions').getPaths(q).length === 0) {
                                        return false;
                                    }
                                    return true;
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                                $.each(data.reportData, function (i, row) {
                                    regionDict[data.reportMetadata[i].region] = $.extend(row, data.reportMetadata[i], {
                                        curMetric: quantify(row, metric)
                                    });
    
                                var metric = $$('.userCountryMapSelectMetrics').val();
    
                                if (UserCountryMap.aggregate[iso]) {
                                    var aggregated = aggregate(regionDict, function (row) {
                                        var id = row.region, res = false;
                                        $.each(UserCountryMap.aggregate[iso].groups, function (group, codes) {
                                            if ($.inArray(id, codes) > -1) {
                                                res = group;
                                            }
                                        });
                                        return res;
    
                                    //if (!UserCountryMap.aggregate.partial) regionDict = {};
                                    $.each(aggregated, function (id, group) {
                                        group.curMetric = quantify(group, metric);
                                        regionDict[id] = group;
                                    });
                                }
    
                                $.each(regionDict, function (key, region) {
                                    if (regionExistsInMap(key)) unlocated -= region.nb_visits;
    
                                displayUnlocatableCount(unlocated, totalCountryVisits);
    
                                // create color scale
                                colscale = getColorScale(regionDict, 'curMetric', null, true);
    
                                function regionFill(data) {
                                    var code = regionCode(data);
                                    return regionDict[code] === undefined ? '#fff' : colscale(regionDict[code].curMetric);
                                }
    
                                // apply colors to map
                                map.getLayer('regions')
                                    .style('fill', regionFill)
                                    .style('stroke',function (data) {
                                        return regionDict[regionCode(data)] === undefined ? '#bbb' : '#3C6FB6';
                                    }).sort(function (data) {
                                        var code = regionCode(data);
                                        return regionDict[code] === undefined ? -1 : regionDict[code].curMetric;
                                    }).tooltips(function (data) {
                                        var metric = $$('.userCountryMapSelectMetrics').val(),
                                            region = regionDict[regionCode(data)];
                                        if (region === undefined) {
    
                                            return '<h3>' + data.name + '</h3><p>' + _.nb_visits.replace('%s', '<strong>0</strong>') + '</p>';
    
                                        }
                                        return '<h3>' + data.name + '</h3>' +
                                            formatValueForTooltips(region, metric, iso);
                                    }).on('click',function (d, path, evt) {
                                        var region = regionDict[regionCode(d)];
                                        if (region && region.label) {
                                            if (evt.shiftKey) {
                                                path.attr('fill', '#f4f45b');
                                                addMultipleRowEvolution('getRegion', region.label);
                                            } else {
                                                map.getLayer('regions').style('fill', regionFill);
                                                showRowEvolution('getRegion', region.label);
                                            }
                                        }
                                    }).on('mouseenter',function (d, path, evt) {
                                        var region = regionDict[regionCode(d)];
                                        if (region && region.label) {
                                            if (evt.shiftKey) {
                                                path.attr('fill', '#f4f45b');
                                            }
                                        }
                                    }).on('mouseleave',function (d, path, evt) {
                                        var region = regionDict[regionCode(d)];
                                        if (region && region.label) {
                                            if ($.inArray(region.label, _rowEvolution.labels) == -1) {
                                                // reset color
                                                path.attr('fill', regionFill(d));
                                            }
                                        }
                                    }).style('cursor', function (d) {
                                        return regionDict[regionCode(d)] && regionDict[regionCode(d)].label ? 'pointer' : 'default';
                                    });
    
                                // check for regions missing in the map
                                $.each(regionDict, function (code, region) {
                                    if (!regionExistsInMap(code)) {
                                        console.warn('possible region mismatch!', code, region.nb_visits);
    
                            });
                    }
    
                    /*
                     * updates the city symbols in the current map
                     * this happens once a new country is loaded and
                     * whenever the metric changes
                     */
                    function updateCitySymbols() {
                        // color regions in white as background for symbols
                        if (map.getLayer('regions')) map.getLayer('regions').style('fill', '#fff');
    
                        indicateLoading();
    
                        // get visits per city from API
                        ajax(_reportParams('UserCountry', 'getCity', UserCountryMap.countriesByIso[iso].iso2))
    
                                var metric = $$('.userCountryMapSelectMetrics').val(),
                                    colscale,
                                    totalCountryVisits = UserCountryMap.countriesByIso[iso].nb_visits,
                                    unlocated = totalCountryVisits,
                                    cities = [];
    
                                // merge reportData and reportMetadata to cities array
                                $.each(data.reportData, function (i, row) {
                                    unlocated -= row.nb_visits;
                                    cities.push($.extend(row, data.reportMetadata[i], {
                                        curMetric: quantify(row, metric)
                                    }));
                                });
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                                displayUnlocatableCount(unlocated, totalCountryVisits);
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                                // sort by current metric
                                cities.sort(function (a, b) { return b.curMetric - a.curMetric; });
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                                colscale = getColorScale(cities, metric);
    
                                // construct scale
                                var radscale = $K.scale.linear(cities.concat({ curMetric: 0 }), 'curMetric');
    
                                var area = map.container.width() * map.container.height(),
                                    sumArea = 0,
                                    f = {
                                        nb_visits: 0.002,
                                        nb_actions: 0.002,
                                        avg_time_on_site: 0.02,
                                        nb_actions_per_visit: 0.02,
                                        bounce_rate: 0.02
                                    },
                                    maxRad;
    
                                $.each(cities, function (i, city) {
                                    sumArea += isNaN(city.curMetric) ? 0 : Math.pow(radscale(city.curMetric), 2);
                                });
                                maxRad = Math.sqrt(area * f[metric] / sumArea);
    
                                radscale = $K.scale.sqrt(cities.concat({ curMetric: 0 }), 'curMetric').range([2, maxRad + 2]);
    
                                var is_rate = metric.substr(0, 3) != 'nb_' || metric == 'nb_actions_per_visit';
    
                                var citySymbols = map.addSymbols({
                                    type: Kartograph.LabeledBubble,
                                    data: cities,
                                    clustering: 'noverlap',
                                    clusteringOpts: {
                                        size: 128,
                                        tolerance: 0
                                    },
                                    title: function (d) {
                                        return radscale(d.curMetric) > 10 ? formatNumber(d.curMetric) : '';
                                    },
                                    labelattrs: {
                                        fill: '#fff',
                                        'font-size': 11,
                                        stroke: false,
    
                                        cursor: 'pointer'
    
                                    },
                                    filter: function (d) {
                                        if (isNaN(d.lat) || isNaN(d.long)) return false;
                                        return is_rate ? d.nb_visits > 5 && d.curMetric : d.curMetric;
                                    },
                                    aggregate: function (rows) {
                                        var row = aggregate(rows);
                                        row.city_names = [];
                                        row.label = rows[0].label; // keep label of biggest city for row evolution
                                        $.each(rows, function (i, r) {
                                            row.city_names = row.city_names.concat(r.city_names ? r.city_names : [r.city_name]);
                                        });
                                        row.city_name = row.city_names[0] + (row.city_names.length > 1 ? ' ' + _.and_n_others.replace('%s', (row.city_names.length - 1)) : '');
                                        row.curMetric = quantify(row, metric);
                                        return row;
                                    },
                                    sortBy: 'radius desc',
                                    location: function (city) { return [city.long, city.lat]; },
                                    radius: function (city) { return radscale(city.curMetric); },
                                    tooltip: function (city) {
                                        return '<h3>' + city.city_name + '</h3>' +
                                            formatValueForTooltips(city, metric, iso);
                                    },
                                    attrs: function (city) {
                                        return {
                                            fill: colscale(city.curMetric).hex(),
                                            'fill-opacity': 0.7,
                                            stroke: '#fff',
                                            cursor: 'pointer'
                                        };
                                    },
                                    mouseenter: function (city, symbol, evt) {
                                        symbol.path.attr({
                                            'fill-opacity': 1,
                                            'stroke': '#000000',
                                            'stroke-opacity': 1,
                                            'stroke-width': 2
    
                                        if (evt.shiftKey) {
                                            symbol.path.attr({ fill: '#f4f45b' });
                                            if (symbol.label) symbol.label.attr({ fill: '#000' });
                                        }
                                    },
                                    mouseleave: function (city, symbol) {
                                        symbol.path.attr({
                                            'fill-opacity': 0.7,
                                            'stroke-opacity': 1,
                                            'stroke-width': 1,
                                            'stroke': '#ffffff'
                                        });
                                        if ($.inArray(city.label, _rowEvolution.labels) == -1) {
                                            symbol.path.attr({ fill: colscale(city.curMetric) });
                                            if (symbol.label) symbol.label.attr({ fill: '#fff' });
                                        }
                                    },
                                    click: function (city, symbol, evt) {
                                        if (evt.shiftKey) {
                                            addMultipleRowEvolution('getCity', city.label);
                                            symbol.path.attr('fill', '#f4f45b');
                                            if (symbol.label) symbol.label.attr('fill', '#000');
                                        } else {
                                            showRowEvolution('getCity', city.label);
                                            citySymbols.update({
                                                attrs: function (city) {
                                                    return { fill: colscale(city.curMetric) };
                                                }
                                            });
                                        }
    
                    _updateMap(iso + '.svg', function () {
    
    
                        // add background
                        map.addLayer('context', {
                            key: 'iso',
    
                                return UserCountryMap.countriesByIso[pd.iso] === undefined;
                            }
                        });
                        map.addLayer('context', {
                            key: 'iso',
                            name: 'context-clickable',
    
                                return UserCountryMap.countriesByIso[pd.iso] !== undefined;
                            },
    
                            click: function (path, p, evt) {   // add click events for surrounding countries
    
                                evt.stopPropagation();
                                updateState(path.iso);
                            },
    
                                if (UserCountryMap.countriesByIso[data.iso] === undefined) {
                                    return 'no data';
                                }
                                var metric = $$('.userCountryMapSelectMetrics').val(),
                                    country = UserCountryMap.countriesByIso[data.iso];
    
                                return '<h3>' + country.name + '</h3>' +
    
                                    formatValueForTooltips(country, metric, 'world');
                            }
                        });
                        function isThisCountry(d) { return d.iso == iso;}
    
                        map.addLayer("context", {
                            name: "regionBG",
                            filter: isThisCountry
                        });
                        map.addLayer("context", {
                            name: "regionBG-fill",
                            filter: isThisCountry
                        });
                        map.addLayer('regions', {
                            key: 'fips',
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                            name: self.mode != "region" ? "regions2" : "regions",
    
                            styles: {
                                stroke: '#aaa'
                            },
    
                                evt.stopPropagation();
                            }
                        });
                        function filtCountryLabels(data) {
                            return data.iso != iso &&
                                map.getLayer('context-clickable') &&
                                map.getLayer('context-clickable').getPath(data.iso) &&
                                Math.abs(map.getLayer('context-clickable').getPath(data.iso).path.area()) > 700;
    
                        // returns either the reference to the country polygon or a custom label
                        // position if defined in UserCountryMap.customLabelPositions
                        function countryLabelPos(data) {
                            var CLP = UserCountryMap.customLabelPositions;
                            if (CLP[iso] && CLP[iso][data.iso]) return CLP[iso][data.iso];
    
                            return 'context-clickable.' + data.iso;
    
                        map.addSymbols({
                            data: map.getLayer('context-clickable').getPathsData(),
                            type: $K.Label,
                            filter: filtCountryLabels,
                            location: countryLabelPos,
    
                            text: function (data) { return UserCountryMap.countriesByIso[data.iso].iso2; },
    
                            'class': 'countryLabelBg'
                        });
                        map.addSymbols({
                            data: map.getLayer('context-clickable').getPathsData(),
                            type: $K.Label,
                            filter: filtCountryLabels,
                            location: countryLabelPos,
    
                            text: function (data) { return UserCountryMap.countriesByIso[data.iso].iso2; },
    
                            'class': 'countryLabel'
                        });
    
                        if (!UserCountryMap.countriesByIso[iso]) return;
    
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                        if (self.mode == "region") {
    
                            updateRegionColors();
                        } else {
                            updateCitySymbols();
                        }
    
                    });
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
                }
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                var _rowEvolution = { labels: [], method: false };
    
    Gregor Aisch's avatar
    Gregor Aisch a validé
    
    
                function addMultipleRowEvolution(method, label) {