Newer
Older
var map = $K.map('#UserCountryMap_map'),
main = $('#UserCountryMap_container'),
UserCountryMap.widget = $('#widgetUserCountryMapvisitorMap').parent();
function _reportParams(module, action, countryFilter) {
var params = $.extend(UserCountryMap.reqParams, {
module: 'API',
method: 'API.getProcessedReport',
apiModule: module,
apiAction: action,
});
if (countryFilter) {
$.extend(params, {
filter_column: 'country',
filter_sort_column: 'nb_visits',
filter_pattern: countryFilter
});
}
return params;
}
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)],
function onResize() {
var ratio, w, h;
ratio = map.viewAB.width / map.viewAB.height;
w = map.container.width();
h = w / ratio;
map.container.height(h-2);
map.resize(w, h);
if (w < 355) $('.tableIcon span').hide();
else $('.tableIcon span').show();
function formatNumber(v) {
v = Number(v);
return v > 1000000 ? (v/1000000).toFixed(1) + 'm' :
v > 1000 ? (v/1000).toFixed(1) + 'k' :
v;
}
//
// 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
function formatValueForTooltips(data, metric, id) {
var val = data[metric] % 1 === 0 || Number(data[metric]) != data[metric] ? data[metric] : data[metric].toFixed(1),
v = UserCountryMap._[metric].replace('%s', '<b>'+val+'</b>');
if (val == 1 && metric == 'nb_visits') v = UserCountryMap._.one_visit.replace('%s', '<b>'+val+'</b>');
function avgTime(d) { return d['sum_visit_length'] / d['nb_visits']; }
var total;
if (id.length == 3) total = UserCountryMap.countriesByIso[id][metric];
else {
total = 0;
$.each(UserCountryMap.countriesByIso, function(iso, country) {
if (total) {
v += ' ('+formatPercentage(data[metric] / total)+')';
}
var colscale;
function addLegendItem(val) {
var d = $('<div>'), r = $('<div>'), l = $('<div>'), v = formatNumber(val);
d.css({ width: 17, height: 17, float: 'left', background: colscale.getColor(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 = UserCountryMap.lastSelected, c;
if (!$.isFunction(filter) || filter(r)) {
var v = quantify(r, metric);
if (!isNaN(v)) values.push(v);
}
if (stats.min == stats.max) {
colscale = { getColor: function() { return '#CDDAEF'; } };
if (choropleth) {
$('.UserCountryMap-legend .content').html('').show();
addLegendItem(stats.min);
}
return colscale;
}
colscale = new chroma.ColorScale({
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 = new chroma.ColorScale({
colors: ['#385993', '#385993','#E87500', '#E87500'],
limits: chroma.limits(rows, 'c', 5, 'curMetric', filter),
positions: [0, c, c+0.001, 1],
mode: 'hsl'
});
}
if (itemExists[v]) return;
addLegendItem(v);
itemExists[v] = true;
/*
* 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(UserCountryMap._resizeTimer);
UserCountryMap._resizeTimer = setTimeout(onResize, 300);
}
function activateButton(btn) {
$('#UserCountryMap-view-mode-buttons a').removeClass('activeIcon');
btn.addClass('activeIcon');
$('#UserCountryMap-activeItem').offset({ left: btn.offset().left });
}
function initUserInterface() {
// react to changes of country select
$('#userCountryMapSelectCountry').change(function() {
updateState($('#userCountryMapSelectCountry').val());
});
var t = UserCountryMap.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);
}
// enable zoom-out
$('#UserCountryMap-btn-zoom').click(zoomOut);
$('#UserCountryMap_map').click(zoomOut);
// handle window resizes
$(window).resize(onResizeLazy);
// enable mertic changes
$('#userCountryMapSelectMetrics').change(function() {
updateState(UserCountryMap.lastSelected);
});
// handle city button
(function(btn) {
btn.click(function() {
if (UserCountryMap.lastSelected.length == 3) {
if (UserCountryMap.mode != "city") {
UserCountryMap.mode = "city";
// handle region button
(function(btn) {
btn.click(function() {
if (UserCountryMap.mode != "region") {
$('#UserCountryMap-view-mode-buttons a').removeClass('activeIcon');
UserCountryMap.mode = "region";
// add loading indicator overlay
var bl = $('<div id="UserCountryMap-black"></div>');
bl.hide();
$('#UserCountryMap_map').append(bl);
var infobtn = $('.UserCountryMap-info-btn');
infobtn.on('mouseenter', function(e) {
$(infobtn.data('tooltip-target')).show();
}).on('mouseleave', function(e) {
$(infobtn.data('tooltip-target')).hide();
});
$('.UserCountryMap-tooltip').hide();
* updateState, called whenever the view changes
// double check view mode
if (UserCountryMap.mode == "city" && id.length != 3) {
// city mode is reserved for country views
UserCountryMap.mode = "region";
}
try {
// check which map to render
if (id.length == 3) {
// country map
renderCountryMap(id, metric);
} else {
// world or continent map
renderWorldMap(id, metric);
}
$('.UserCountryMap-info .content').html(e);
$('.UserCountryMap-info').show();
UserCountryMap.lastSelected = id;
}
/*
* update the widgets ui according to the currently selected view
*/
if (UserCountryMap.mode == "city") {
activateButton($('#UserCountryMap-btn-city'));
} else {
activateButton($('#UserCountryMap-btn-region'));
}
var countrySelect = $('#userCountryMapSelectCountry');
countrySelect.val(id);
if (id == 'world') zoom.addClass('inactiveIcon');
else zoom.removeClass('inactiveIcon');
var flag = $('#userCountryMapFlag'),
regionBtn = $('#UserCountryMap-btn-region');
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(UserCountryMap._.no_data);
$('.map-title').html('');
return;
}
$('#UserCountryMap-btn-city').addClass('inactiveIcon').hide();
$('span', regionBtn).html(regionBtn.data('country'));
var mapTitle = id.length == 3 ?
UserCountryMap.countriesByIso[id].name :
$('#userCountryMapSelectCountry option[value='+id+']').html(),
totalVisits = 0;
// update map title
$('.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 = UserCountryMap.config.visitsSummary['nb_visits'];
}
if (id.length == 3) {
$('.map-stats').html(formatValueForTooltips(UserCountryMap.countriesByIso[id], metric, 'world'));
} else {
$('.map-stats').html(
UserCountryMap._.nb_visits.replace('%s', '<b>'+formatNumber(totalVisits) + '</b>') +(id != 'world' ? ' ('+
formatPercentage(totalVisits / worldTotalVisits)+')' : '')
);
/*
* 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;
} else {
return true;
}
var d = UserCountryMap.countriesByIso[data.iso];
if (d === null) {
return UserCountryMap.config.noDataColor;
} else {
}
// 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
}
});
map.getLayer('countries').tooltips(function(data) {
var metric = $('#userCountryMapSelectMetrics').val(),
country = UserCountryMap.countriesByIso[data.iso];
return '<h3>'+country.name + '</h3>'+
formatValueForTooltips(country, metric, target);
});
}
// if the view hasn't changed (but probably the selected metric),
// all we need to do is to recolor the current map.
if (target == UserCountryMap.lastSelected) {
updateColorsAndTooltips(metric);
return;
}
// 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',
filter: function(pd) {
return UserCountryMap.countriesByIso[pd.iso] === undefined;
},
tooltips: function(pd) {
// 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',
filter: function(pd) {
return UserCountryMap.countriesByIso[pd.iso] !== undefined;
},
click: function(data, path, evt) {
evt.stopPropagation();
addMultipleRowEvolution('getCountry', UserCountryMap.countriesByIso[data.iso].name);
} else {
showRowEvolution('getCountry', UserCountryMap.countriesByIso[data.iso].name);
updateColorsAndTooltips(metric);
}
if (UserCountryMap.lastSelected != 'world' || UserCountryMap.countriesByIso[data.iso] === undefined) {
tgt = data.iso;
tgt = UserCountryMap.ISO3toCONT[data.iso];
}
updateState(tgt);
/*
* updateMap is called by renderCountryMap() and renderWorldMap()
*/
function indicateLoading() {
$('#UserCountryMap-black').show();
$('#UserCountryMap-black').css('opacity', 0);
$('#UserCountryMap .loadingPiwik').show();
}
function loadingComplete() {
$('#UserCountryMap-black').hide();
$('#UserCountryMap .loadingPiwik').hide();
/*
* 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];
}
}
/*
* 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.
*/
var groups = {};
$.each(rows, function(i, row) {
g_id = g_id === true ? $.isNumeric(i) && i === Number(i) ? false : i : g_id;
if (!groups[g_id]) {
groups[g_id] = {
nb_visits: 0,
nb_actions: 0,
sum_visit_length: 0,
bounce_count: 0
};
}
$.each(groups[g_id], function(metric) {
groups[g_id][metric] += row[metric];
});
$.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)+"%";
});
$('.unlocated-stats').html(
$('.unlocated-stats').data('tpl')
.replace('%c', UserCountryMap.countriesByIso[UserCountryMap.lastSelected].name)
/*
* renders a country map (either region or city view)
*/
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
*/
indicateLoading();
// load data from Piwik API
url: 'index.php',
type: 'POST',
data: _reportParams('UserCountry', 'getRegion', UserCountryMap.countriesByIso[iso].iso2),
var regionDict = {},
totalCountryVisits = UserCountryMap.countriesByIso[iso].nb_visits,
unlocated = totalCountryVisits;
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;
}
$.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;
});
$.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);
colscale = getColorScale(regionDict, 'curMetric', null, true);
var code = regionCode(data);
return regionDict[code] === undefined ? '#fff' : colscale.getColor(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>'+UserCountryMap._.nb_visits.replace('%s', '<b>0</b>')+'</p>';
}
return '<h3>'+data.name+'</h3>'+
formatValueForTooltips(region, metric, iso);
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 (evt.shiftKey) {
path.attr('fill', '#f4f45b');
}
}
}).on('mouseleave', function(d, path, evt) {
var region = regionDict[regionCode(d)];
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) {
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
*/
// 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
url: 'index.php',
data: _reportParams('UserCountry', 'getCity', UserCountryMap.countriesByIso[iso].iso2),
var metric = $('#userCountryMapSelectMetrics').val(),
colscale,
totalCountryVisits = UserCountryMap.countriesByIso[iso].nb_visits,
unlocated = totalCountryVisits,
// merge reportData and reportMetadata to cities array
$.each(data.reportData, function(i, row) {
cities.push($.extend(row, data.reportMetadata[i], {
curMetric: quantify(row, metric)
}));
// sort by current metric
cities.sort(function(a, b) { return b.curMetric - a.curMetric; });
colscale = getColorScale(cities, metric);
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';
title: function(d) {
return radscale(d.curMetric) > 10 ? formatNumber(d.curMetric) : '';
},
labelattrs: {
fill: '#fff',
'font-size': 11,
return is_rate ? d.nb_visits > 5 && d.curMetric : d.curMetric;
},
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 ? ' '+UserCountryMap._.and_n_others.replace('%s', (row.city_names.length-1)) : '');
row.curMetric = quantify(row, metric);
return row;
},
radius: function(city) { return radscale(city.curMetric); },
return '<h3>'+city.city_name+'</h3>'+
formatValueForTooltips(city, metric, iso);
attrs: function(city) {
return {
fill: colscale.getColor(city.curMetric),
'fill-opacity': 0.7,
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.getColor(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.evaluate({
attrs: function(city) {
return { fill: colscale.getColor(city.curMetric) };
}
});
}
}
map.addLayer('context', {
key: 'iso',
filter: function(pd) {
return UserCountryMap.countriesByIso[pd.iso] === undefined;
}
});
map.addLayer('context', {
key: 'iso',
name: 'context-clickable',
filter: function(pd) {
return UserCountryMap.countriesByIso[pd.iso] !== undefined;
},
click: function(path, p, evt) { // add click events for surrounding countries
evt.stopPropagation();
updateState(path.iso);
},
tooltips: function(data) {
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',
name: UserCountryMap.mode != "region" ? "regions2" : "regions",
click: function(d, p, evt) {
evt.stopPropagation();
}
return data.iso != 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;
}
text: function(data) { return UserCountryMap.countriesByIso[data.iso].iso2; },
'class': 'countryLabelBg'
});
map.addSymbols({
data: map.getLayer('context-clickable').getPathsData(),
type: $K.Label,
filter: filtCountryLabels,
text: function(data) { return UserCountryMap.countriesByIso[data.iso].iso2; },
if (UserCountryMap.mode == "region") {
updateRegionColors();
} else {
updateCitySymbols();
}
function addMultipleRowEvolution(method, label) {
if (method != _rowEvolution.method) {
_rowEvolution = { method: method, labels: [] };
}
var box = Piwik_Popover.showLoading('Row Evolution'),
multiple;
multiple = method == _rowEvolution.method && _rowEvolution.labels.length > 0;
if (multiple) {
_rowEvolution.labels.push(label);
$.each(_rowEvolution.labels, function(i,l) {
_rowEvolution.labels[i] = l.replace(/, /g, '%2C%20');
});
var requestParams = $.extend(UserCountryMap.reqParams, {
apiMethod: 'UserCountry.' + method,
label: multiple ? _rowEvolution.labels.join(',') : label.replace(/, /g, '%2C%20'),
action: multiple ? 'getMultiRowEvolutionPopover' : 'getRowEvolutionPopover'
$.ajax({
url: 'index.php',
type: 'POST',
dataType: 'html',