/*!
* Matomo - free/libre analytics platform
*
* Visitors Map with zoom in continents / countries. Cities + Region view.
* Using Kartograph.js http://kartograph.org/
*
* @link https://matomo.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
// create a global namespace for UserCountryMap plugin
// this is used both by visitor map and realtime map
window.UserCountryMap = window.UserCountryMap || {};
// 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, {
/*
* initializes the map after widget creation
*/
run: function () {
var self = this,
config = self.config,
colorManager = piwik.ColorManager,
colorNames = ['no-data-color', 'one-country-color', 'color-range-start-choropleth',
'color-range-start-normal', 'color-range-end-choropleth', 'color-range-end-normal',
'country-highlight-color', 'unknown-region-fill-color', 'unknown-region-stroke-color',
'region-stroke-color', 'invisible-region-background', 'city-label-color',
'city-stroke-color', 'city-highlight-stroke-color', 'city-highlight-fill-color',
'city-highlight-label-color', 'city-label-fill-color', 'city-selected-color',
'city-selected-label-color', 'region-layer-stroke-color', 'country-selected-color',
'region-selected-color', 'region-highlight-color'],
colors = colorManager.getColors('visitor-map', colorNames),
noDataColor = colors['no-data-color'],
oneCountryColor = colors['one-country-color'],
colorRangeStartChoropleth = colors['color-range-start-choropleth'],
colorRangeStartNormal = colors['color-range-start-normal'],
colorRangeEndChoropleth = colors['color-range-end-choropleth'],
colorRangeEndNormal = colors['color-range-end-normal'],
specialMetricsColorScale = colorManager.getColors(
'visitor-map',
['special-metrics-color-scale-1', 'special-metrics-color-scale-2', 'special-metrics-color-scale-3',
'special-metrics-color-scale-4'],
true
),
countryHighlightColor = colors['country-highlight-color'],
countrySelectedColor = colors['country-selected-color'],
unknownRegionFillColor = colors['unknown-region-fill-color'],
unknownRegionStrokeColor = colors['unknown-region-stroke-color'],
regionStrokeColor = colors['region-stroke-color'],
regionSelectedColor = colors['region-selected-color'],
regionHighlightColor = colors['region-highlight-color'],
invisibleRegionBackgroundColor = colors['invisible-region-background'],
cityLabelColor = colors['city-label-color'],
cityLabelFillColor = colors['city-label-fill-color'],
cityStrokeColor = colors['city-stroke-color'],
cityHighlightStrokeColor = colors['city-highlight-stroke-color'],
cityHighlightFillColor = colors['city-highlight-fill-color'],
cityHighlightLabelColor = colors['city-highlight-label-color'],
citySelectedColor = colors['city-selected-color'],
citySelectedLabelColor = colors['city-selected-label-color'],
regionLayerStrokeColor = colors['region-layer-stroke-color'],
hasUserZoomed = false;
/*
* our own custom selector to only select stuff of this widget
*/
function $$(selector) {
return $(selector, self.theWidget ? self.theWidget.element : undefined);
}
var mapContainer = $$('.UserCountryMap_map').get(0),
map = self.map = $K.map(mapContainer),
main = $$('.UserCountryMap_container'),
width = main.width(),
_ = config._;
config.noDataColor = noDataColor;
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,
format_metrics: 0,
showRawMetrics: 1
});
if (countryFilter) {
$.extend(params, {
filter_column: 'country',
filter_sort_column: 'nb_visits',
filter_pattern: countryFilter
});
}
return params;
}
/*
* 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, force_api_session: broadcast.isWidgetizeRequestWithoutSession() ? 0 : 1 },
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)]
};
}
function formatNumber(v, metric, first) {
v = Number(v);
if (v > 1000000) {
return (v / 1000000).toFixed(1) + 'm';
}
if (v > 1000) {
return (v / 1000).toFixed(1) + 'k';
}
if (!metric) {
return v;
}
if (metric == 'avg_time_on_site') {
v += first ? ' sec' : 's';
} else if (metric == 'bounce_rate') {
v += '%';
} else if (metric === 'nb_actions_per_visit') {
if (parseInt(v, 10) === v) {
return v;
}
return v.toFixed(1);
}
return 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
// METRIC_raw
//
function formatValueForTooltips(data, metric, id) {
var val = data[metric] % 1 === 0 || Number(data[metric]) != data[metric] ? data[metric] : data[metric].toFixed(1);
if (metric == 'bounce_rate') {
val = NumberFormatter.formatPercent(val);
} else if (metric == 'avg_time_on_site') {
val = new Date(0, 0, 0, val / 3600, val % 3600 / 60, val % 60)
.toTimeString()
.replace(/.*(\d{2}:\d{2}:\d{2}).*/, "$1");
} else {
val = NumberFormatter.formatNumber(val);
}
var v = _[metric].replace('%s', '' + val + '');
if (val == 1 && metric == 'nb_visits') v = _.one_visit;
if (metric.slice(0, 3) == 'nb_' && metric != 'nb_actions_per_visit') {
var total;
if (id.length == 3) total = UserCountryMap.countriesByIso[id][metric];
else if (id == 'world') total = self.config.visitsSummary[metric];
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 += ' (' + _.nb_visits.replace('%s', data.nb_visits) + ')';
}
return v;
}
function getColorScale(rows, metric, filter, choropleth) {
var colscale;
function addLegendItem(val, first) {
var d = $('
'), r = $('
'), l = $('
'),
metric = $$('.userCountryMapSelectMetrics').val(),
v = formatNumber(Math.round(val), metric, first);
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, showLegend;
$.each(rows, function (i, r) {
if (!$.isFunction(filter) || filter(r)) {
var v = quantify(r, metric);
if (!isNaN(v)) values.push(v);
}
});
stats = minmax(values);
showLegend = values.length > 0;
if (stats.min == stats.max) {
colscale = function () { return chroma.hex(oneCountryColor); };
if (choropleth) {
$('.UserCountryMap-legend .content').html('').show();
if (showLegend) {
addLegendItem(stats.min, true);
}
}
return colscale;
}
colscale = chroma.scale()
.range([choropleth ? colorRangeStartChoropleth : colorRangeStartNormal,
choropleth ? colorRangeEndChoropleth : colorRangeEndNormal])
.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(specialMetricsColorScale, [0, c, c + 0.001, 1])
.domain(chroma.limits(rows, 'c', 5, 'curMetric', filter), 4, 'c')
.mode('hsl');
}
}
// a good place to update the legend, isn't it?
if (choropleth && showLegend) {
$('.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();
}
return colscale;
}
function formatPercentage(val) {
if (val < 0.001) {
return '< ' + NumberFormatter.formatPercent(0.1);
}
return NumberFormatter.formatPercent(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);
}
// Save a reference to the function so it can be cleanly removed
// as a listener later.
self._onResizeLazy = onResizeLazy;
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').off('change').change(function () {
hasUserZoomed = true;
updateState($$('.userCountryMapSelectCountry').val());
});
function zoomOut() {
hasUserZoomed = true;
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);
}
// enable zoom-out
$$('.UserCountryMap-btn-zoom').off('click').click(zoomOut);
$$('.UserCountryMap_map').off('click').click(zoomOut);
// handle window resizes
$(window).resize(onResizeLazy);
// enable metric changes
$$('.userCountryMapSelectMetrics').off('change').change(function () {
updateState(self.lastSelected);
});
// handle city button
(function (btn) {
btn.off('click').click(function () {
if (self.lastSelected.length == 3) {
if (self.mode != "city") {
self.mode = "city";
hasUserZoomed = true;
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";
hasUserZoomed = true;
updateState(self.lastSelected);
}
});
})($$('.UserCountryMap-btn-region'));
// add loading indicator overlay
var bl = $('');
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();
}
/*
* 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";
}
var metric = $$('.userCountryMapSelectMetrics').val();
// store current map state
self.widget.dashboardWidget('setParameters', {
lastMap: id, viewMode: self.mode, lastMetric: metric
});
$('.UserCountryMap-info-btn').hide();
try {
// check which map to render
if (id.length == 3) {
// country map
renderCountryMap(id, metric);
} else {
// world or continent map
renderWorldMap(id, metric);
}
} catch (e) {
// console.error(e);
$('.UserCountryMap-info .content').html(e);
$('.UserCountryMap-info').show();
}
_updateUI(id, metric);
self.lastSelected = id;
}
/*
* 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;
}
} else {
flag.css({
'background': 'none'
});
$$('.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,
totalMetricValue = 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'];
totalMetricValue = UserCountryMap.countriesByIso[id][metric];
} else if (id.length == 2) {
$.each(UserCountryMap.countriesByIso, function (iso, country) {
if (UserCountryMap.ISO3toCONT[iso] == id) {
totalVisits += country['nb_visits'];
totalMetricValue += country[metric];
}
});
} else {
totalVisits = self.config.visitsSummary['nb_visits'];
totalMetricValue = self.config.visitsSummary[metric];
}
var data = {};
data[metric] = totalMetricValue;
$('.map-stats').html(
'' + formatValueForTooltips(data, metric, false) + '' +
(id != 'world' ? (' (' + formatPercentage(totalMetricValue / self.config.visitsSummary[metric]) + ')') : '')
);
}
/*
* 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;
}
}, true);
function countryFill(data) {
var d = UserCountryMap.countriesByIso[data.iso];
if (d === null) {
return self.config.noDataColor;
} else {
return colscale(d[metric]);
}
}
var countryLayer = map.getLayer('countries');
if(countryLayer) {
// Apply the color scale to the map.
countryLayer
.style('fill', countryFill)
.on('mouseenter', function (d, path, evt) {
if (evt.shiftKey) { // highlight on mouseover with shift pressed
path.attr('fill', countryHighlightColor);
}
})
.on('mouseleave', function (d, path, evt) {
if ($.inArray(UserCountryMap.countriesByIso[d.iso].name, _rowEvolution.labels) == -1) {
path.attr('fill', countryFill(d)); // reset color
}
});
// Update the map tooltips.
countryLayer.tooltips(function (data) {
var metric = $$('.userCountryMapSelectMetrics').val(),
country = UserCountryMap.countriesByIso[data.iso];
return '
' + country.name + '
' +
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 == self.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) {
var countryName = pd.name;
for (var iso in self.config.countryNames) {
if (UserCountryMap.ISO2toISO3[iso.toUpperCase()] == pd.iso) {
countryName = self.config.countryNames[iso];
break;
}
}
return '
' + countryName + '
' + _.no_visit;
}
});
// 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();
if (evt.shiftKey || _rowEvolution.labels.length) {
if (evt.altKey) {
path.attr('fill', countrySelectedColor);
addMultipleRowEvolution('getCountry', UserCountryMap.countriesByIso[data.iso].name);
} else {
showRowEvolution('getCountry', UserCountryMap.countriesByIso[data.iso].name);
updateColorsAndTooltips(metric);
}
return;
}
var tgt;
if (self.lastSelected != 'world' || UserCountryMap.countriesByIso[data.iso] === undefined) {
tgt = data.iso;
} else {
tgt = UserCountryMap.ISO3toCONT[data.iso];
}
hasUserZoomed = true;
updateState(tgt);
}
});
updateColorsAndTooltips(metric);
});
}
/*
* 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();
}
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) {
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.
*/
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
};
}
$.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 / group.nb_visits;
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) + "%";
});
return groupBy ? groups : groups.X;
}
function displayUnlocatableCount(unlocated, total, regionOrCity) {
if (0 == unlocated) {
return;
}
$('.unlocated-stats').html(
_pk_translate('UserCountryMap_Unlocated', [
unlocated,
'(' + formatPercentage(unlocated / total) + ')',
UserCountryMap.countriesByIso[self.lastSelected].name
])
);
$('.UserCountryMap-info-btn').show();
var zoomTitle = '';
if (regionOrCity == 'region') {
zoomTitle = ' ' + _pk_translate('UserCountryMap_WithUnknownRegion', [unlocated]);
} else if (regionOrCity == 'city') {
zoomTitle = ' ' + _pk_translate('UserCountryMap_WithUnknownCity', [unlocated]);
}
if (unlocated && zoomTitle) {
if ($('.map-stats .unlocatableCount').length) {
$('.map-stats .unlocatableCount').html(zoomTitle);
} else {
$('.map-stats').append('' + zoomTitle + '');
}
}
}
/*
* renders a country map (either region or city view)
*/
function renderCountryMap(iso) {
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
ajax(_reportParams('UserCountry', 'getRegion', UserCountryMap.countriesByIso[iso].iso2))
.done(function (data) {
convertBounceRatesToPercents(data);
loadingComplete();
var regionDict = {},
totalCountryVisits = UserCountryMap.countriesByIso[iso].nb_visits,
unlocated = totalCountryVisits;
// self.lastReportMetricStats = {};
function regionCode(region) {
var key = UserCountryMap.keys[iso] || 'fips';
return key.slice(0, 4) == "fips" ? (region[key] || "").slice(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.slice(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) {
var region = data.reportMetadata[i].region;
if (!regionExistsInMap(region)) {
var q = {
'p': region
};
if (map.getLayer('regions').getPaths(q).length) {
region = map.getLayer('regions').getPaths(q)[0].data.fips.slice(2);
}
}
regionDict[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, 'region');
// create color scale
colscale = getColorScale(regionDict, 'curMetric', null, true);
function regionFill(data) {
var code = regionCode(data);
return regionDict[code] === undefined ? unknownRegionFillColor : colscale(regionDict[code].curMetric);
}
// apply colors to map
map.getLayer('regions')
.style('fill', regionFill)
.style('stroke',function (data) {
return regionDict[regionCode(data)] === undefined ? unknownRegionStrokeColor : regionStrokeColor;
}).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 '
' + data.name + '
' + _.nb_visits.replace('%s', '0') + '
';
}
return '
' + data.name + '
' +
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', regionSelectedColor);
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', regionHighlightColor);
}
}
}).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
var layerName = self.mode != "region" ? "regions2" : "regions";
if (map.getLayer(layerName)) map.getLayer(layerName).style('fill', invisibleRegionBackgroundColor);
indicateLoading();
// get visits per city from API
ajax(_reportParams('UserCountry', 'getCity', UserCountryMap.countriesByIso[iso].iso2))
.done(function (data) {
convertBounceRatesToPercents(data);
loadingComplete();
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)
}));
});
displayUnlocatableCount(unlocated, totalCountryVisits, 'city');
// sort by current metric
cities.sort(function (a, b) { return b.curMetric - a.curMetric; });
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_uniq_visitors: 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 citySymbols = map.addSymbols({
type: $K.LabeledBubble,
data: cities,
clustering: 'noverlap',
clusteringOpts: {
size: 128,
tolerance: 0
},
title: function (d) {
var v = d.curMetric;
if (isNaN(v)) {
return '';
}
if (metric === 'bounce_rate') {
v = Number((''+ v).replace('%', ''));
} else if (metric === 'avg_time_on_site') {
v = Number(v);
}
if (isNaN(v)) {
return '';
}
if (radscale(v) > 10) {
return formatNumber(d.curMetric, metric);
}
return '';
},
labelattrs: {
fill: cityLabelColor,
'font-size': 11,
stroke: false,
cursor: 'pointer'
},
filter: function (d) {
if (isNaN(d.lat) || isNaN(d.long)) return false;
return !!d.curMetric && d.curMetric !== '0';
},
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) {
var scale = radscale(city.curMetric);
if (isNaN(scale)) {
return 0.01;
}
return scale;
},
tooltip: function (city) {
return '
' + city.city_name + '
' +
formatValueForTooltips(city, metric, iso);
},
attrs: function (city) {
var color = colscale(city.curMetric);
if (color && color.hex) {
color = color.hex();
}
return {
fill: color,
'fill-opacity': 0.7,
stroke: cityStrokeColor,
cursor: 'pointer'
};
},
mouseenter: function (city, symbol, evt) {
symbol.path.attr({
'fill-opacity': 1,
'stroke': cityHighlightStrokeColor,
'stroke-opacity': 1,
'stroke-width': 2
});
if (evt.shiftKey) {
symbol.path.attr({ fill: cityHighlightFillColor });
if (symbol.label) symbol.label.attr({ fill: cityHighlightLabelColor });
}
},
mouseleave: function (city, symbol) {
symbol.path.attr({
'fill-opacity': 0.7,
'stroke-opacity': 1,
'stroke-width': 1,
'stroke': cityLabelColor
});
if ($.inArray(city.label, _rowEvolution.labels) == -1) {
symbol.path.attr({ fill: colscale(city.curMetric) });
if (symbol.label) symbol.label.attr({ fill: cityLabelFillColor });
}
},
click: function (city, symbol, evt) {
if (evt.shiftKey) {
addMultipleRowEvolution('getCity', city.label);
symbol.path.attr('fill', citySelectedColor);
if (symbol.label) symbol.label.attr('fill', citySelectedLabelColor);
} 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',
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();
hasUserZoomed = true;
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 '