/*global store, Handlebars, CountlyHelpers, countlyGlobal, _, Gauge, d3, moment, countlyTotalUsers, jQuery, filterXSS, mergeWith*/
(function(window, $) {
/**
* Object with common functions to be used for multiple purposes
* @name countlyCommon
* @global
* @namespace countlyCommon
*/
var CommonConstructor = function() {
// Private Properties
var countlyCommon = this;
var _period = (store.get("countly_date")) ? store.get("countly_date") : countlyCommon.DEFAULT_PERIOD || "30days";
var _persistentSettings;
var htmlEncodeOptions = {
"whiteList": {"a": ["href", "class", "target"], "ul": [], "li": [], "b": [], "br": [], "strong": [], "p": [], "span": ["class"], "div": ["class"]},
onTagAttr: function(tag, name, value/* isWhiteAttr*/) {
if (tag === "a") {
var re = new RegExp(/{[0-9]*}/);
var tested = re.test(value);
if (name === "target") {
if (!(value === "_blank" || value === "_self" || value === "_top" || value === "_parent" || tested)) {
return 'target="_blank"'; //set _blank if incorrect value
}
else {
return 'target="' + value + '"';
}
}
if (name === "href") {
if (!(value.substr(0, 1) === "#" || value.substr(0, 1) === "/" || value.substr(0, 4) === "http" || tested)) {
return 'href="#"'; //set # if incorrect value
}
else {
return 'href="' + value + '"';
}
}
}
}
};
/**
* Get Browser language
* @memberof countlyCommon
* @returns {string} browser locale in iso format en-US
* @example
* //outputs en-US
* countlyCommon.browserLang()
*/
countlyCommon.browserLang = function() {
var lang = navigator.language || navigator.userLanguage;
if (lang) {
lang = lang.toLowerCase();
lang.length > 3 && (lang = lang.substring(0, 3) + lang.substring(3).toUpperCase());
}
return lang;
};
// Public Properties
/**
* Set user persistent settings to store local storage
* @memberof countlyCommon
* @param {object} data - Object param for set new data
*/
countlyCommon.setPersistentSettings = function(data) {
if (!_persistentSettings) {
_persistentSettings = localStorage.getItem("persistentSettings") ? JSON.parse(localStorage.getItem("persistentSettings")) : {};
}
for (var i in data) {
_persistentSettings[i] = data[i];
}
localStorage.setItem("persistentSettings", JSON.stringify(_persistentSettings));
};
/**
* Get user persistent settings
* @memberof countlyCommon
* @returns {object} settings
*/
countlyCommon.getPersistentSettings = function() {
if (!_persistentSettings) {
_persistentSettings = localStorage.getItem("persistentSettings") ? JSON.parse(localStorage.getItem("persistentSettings")) : {};
}
return _persistentSettings;
};
/**
* App Key of currently selected app or 0 when not initialized
* @memberof countlyCommon
* @type {string|number}
*/
countlyCommon.ACTIVE_APP_KEY = 0;
/**
* App ID of currently selected app or 0 when not initialized
* @memberof countlyCommon
* @type {string|number}
*/
countlyCommon.ACTIVE_APP_ID = 0;
/**
* Current user's selected language in form en-EN, by default will use browser's language
* @memberof countlyCommon
* @type {string}
*/
countlyCommon.BROWSER_LANG = countlyCommon.browserLang() || "en-US";
/**
* Current user's browser language in short form as "en", by default will use browser's language
* @memberof countlyCommon
* @type {string}
*/
countlyCommon.BROWSER_LANG_SHORT = countlyCommon.BROWSER_LANG.split("-")[0];
if (store.get("countly_active_app")) {
if (countlyGlobal.apps[store.get("countly_active_app")]) {
countlyCommon.ACTIVE_APP_KEY = countlyGlobal.apps[store.get("countly_active_app")].key;
countlyCommon.ACTIVE_APP_ID = store.get("countly_active_app");
}
}
if (countlyGlobal.member.lang) {
var lang = countlyGlobal.member.lang;
store.set("countly_lang", lang);
countlyCommon.BROWSER_LANG_SHORT = lang;
countlyCommon.BROWSER_LANG = lang;
}
else if (store.get("countly_lang")) {
var lang1 = store.get("countly_lang");
countlyCommon.BROWSER_LANG_SHORT = lang1;
countlyCommon.BROWSER_LANG = lang1;
}
// Public Methods
/**
* Change currently selected period
* @memberof countlyCommon
* @param {string|array} period - new period, supported values are (month, 60days, 30days, 7days, yesterday, hour or [startMiliseconds, endMiliseconds] as [1417730400000,1420149600000])
* @param {int} timeStamp - timeStamp for the period based
* @param {boolean} noSet - if false - updates countly_date
*/
countlyCommon.setPeriod = function(period, timeStamp, noSet) {
_period = period;
if (timeStamp) {
countlyCommon.periodObj = countlyCommon.calcSpecificPeriodObj(period, timeStamp);
}
else {
countlyCommon.periodObj = calculatePeriodObject(period);
}
if (window.countlyCommon === this && window.app && window.app.recordEvent) {
window.app.recordEvent({
"key": "period-change",
"count": 1,
"segmentation": {is_custom: Array.isArray(period)}
});
}
if (noSet) {
/*
Dont update vuex or local storage if noSet is true
*/
return;
}
if (window.countlyCommon === this && window.countlyVue && window.countlyVue.vuex) {
var currentStore = window.countlyVue.vuex.getGlobalStore();
if (currentStore) {
currentStore.dispatch("countlyCommon/updatePeriod", {period: period, label: countlyCommon.getDateRangeForCalendar()});
}
}
store.set("countly_date", period);
};
/* Returns strings representing dates, not timestamps*/
countlyCommon.getPeriodAsDateStrings = function() {
var array = [];
if (Array.isArray(_period)) {
if (countlyCommon.periodObj.currentPeriodArr && countlyCommon.periodObj.currentPeriodArr.length > 0) {
var splitted = countlyCommon.periodObj.currentPeriodArr[0].split(".");
array.push(splitted[2] + "-" + splitted[1] + "-" + splitted[0] + " 00:00:00");
splitted = countlyCommon.periodObj.currentPeriodArr[countlyCommon.periodObj.currentPeriodArr.length - 1].split(".");
array.push(splitted[2] + "-" + splitted[1] + "-" + splitted[0] + " 23:59:59");
}
return JSON.stringify(array);
}
else {
return countlyCommon.getPeriodForAjax();
}
};
/**
* Get currently selected period
* @memberof countlyCommon
* @returns {string|array} supported values are (month, 60days, 30days, 7days, yesterday, hour or [startMiliseconds, endMiliseconds] as [1417730400000,1420149600000])
*/
countlyCommon.getPeriod = function() {
return _period;
};
countlyCommon.removePeriodOffset = function(period) {
var newPeriod = period;
if (Array.isArray(period)) {
newPeriod = [];
newPeriod[0] = period[0] + countlyCommon.getOffsetCorrectionForTimestamp(period[0]);
newPeriod[1] = period[1] + countlyCommon.getOffsetCorrectionForTimestamp(period[1]);
}
return newPeriod;
};
countlyCommon.getPeriodWithOffset = function(period) {
var newPeriod = period;
if (Array.isArray(period)) {
newPeriod = [];
newPeriod[0] = period[0] - countlyCommon.getOffsetCorrectionForTimestamp(period[0]);
newPeriod[1] = period[1] - countlyCommon.getOffsetCorrectionForTimestamp(period[1]);
}
return newPeriod;
};
countlyCommon.getPeriodForAjax = function() {
return CountlyHelpers.getPeriodUrlQueryParameter(countlyCommon.getPeriodWithOffset(_period));
};
/**
* Change currently selected app by app ID
* @memberof countlyCommon
* @param {string} appId - new app ID from @{countlyGlobal.apps} object
*/
countlyCommon.setActiveApp = function(appId) {
countlyCommon.ACTIVE_APP_KEY = countlyGlobal.apps[appId].key;
countlyCommon.ACTIVE_APP_ID = appId;
store.set("countly_active_app", appId);
$.ajax({
type: "POST",
url: countlyGlobal.path + "/user/settings/active-app",
data: {
"username": countlyGlobal.member.username,
"appId": appId,
_csrf: countlyGlobal.csrf_token
},
success: function() { }
});
};
/**
* Adds notification toast to the list.
* @param {*} payload notification toast
* payload.color: color of the notification toast
* payload.text: text of the notification toast
*/
countlyCommon.dispatchNotificationToast = function(payload) {
if (window.countlyVue && window.countlyVue.vuex) {
var currentStore = window.countlyVue.vuex.getGlobalStore();
if (currentStore) {
currentStore.dispatch('countlyCommon/onAddNotificationToast', payload);
}
}
};
/**
* Adds a new notification to persistent notification list.
* @param {*} payload notification payload
* payload.color: color of the notification
* payload.text: text of the notification
*/
countlyCommon.dispatchPersistentNotification = function(payload) {
if (window.countlyVue && window.countlyVue.vuex) {
var currentStore = window.countlyVue.vuex.getGlobalStore();
if (currentStore) {
currentStore.dispatch("countlyCommon/onAddPersistentNotification", payload);
}
}
};
/**
* Removes a notification from persistent notification list based on id.
* @param {string} notificationId notification id
*/
countlyCommon.removePersistentNotification = function(notificationId) {
if (window.countlyVue && window.countlyVue.vuex) {
var currentStore = window.countlyVue.vuex.getGlobalStore();
if (currentStore) {
currentStore.dispatch("countlyCommon/onRemovePersistentNotification", notificationId);
}
}
};
/**
* Generates unique id string using unsigned integer array.
* @returns {string} unique id
*/
countlyCommon.generateId = function() {
var crypto = window.crypto || window.msCrypto;
var uint32 = crypto.getRandomValues(new Uint32Array(1))[0];
return uint32.toString(16);
};
/**
*
* @param {name} name drawer name
* @returns {object} drawer data used by hasDrawers() mixin
*/
countlyCommon.getExternalDrawerData = function(name) {
var result = {};
result[name] = {
name: name,
isOpened: false,
initialEditedObject: {},
};
result[name].closeFn = function() {
result[name].isOpened = false;
};
return result;
};
/**
* Encode value to be passed to db as key, encoding $ symbol to $ if it is first and all . (dot) symbols to . in the string
* @memberof countlyCommon
* @param {string} str - value to encode
* @returns {string} encoded string
*/
countlyCommon.encode = function(str) {
return str.replace(/^\$/g, "$").replace(/\./g, '.');
};
/**
* Decode value from db, decoding first $ to $ and all . to . (dots). Decodes also url encoded values as $.
* @memberof countlyCommon
* @param {string} str - value to decode
* @returns {string} decoded string
*/
countlyCommon.decode = function(str) {
return str.replace(/^$/g, "$").replace(/./g, '.');
};
/**
* Decode escaped HTML from db
* @memberof countlyCommon
* @param {string} html - value to decode
* @returns {string} decoded string
*/
countlyCommon.decodeHtml = function(html) {
return (html + "").replace(/&/g, '&');
};
/**
* Encode html
* @memberof countlyCommon
* @param {string} html - value to encode
* @returns {string} encode string
*/
countlyCommon.encodeHtml = function(html) {
var div = document.createElement('div');
div.innerText = html;
return div.innerHTML;
};
countlyCommon.unescapeHtml = function(htmlStr) {
if (htmlStr && typeof htmlStr === "string") {
htmlStr = htmlStr.replace(/</g, "<");
htmlStr = htmlStr.replace(/>/g, ">");
htmlStr = htmlStr.replace(/"/g, "\"");
htmlStr = htmlStr.replace(/'/g, "\'");
htmlStr = htmlStr.replace(/&/g, "&");
}
return htmlStr;
};
/**
* Encode some tags, leaving those set in whitelist as they are.
* @memberof countlyCommon
* @param {string} html - value to encode
* @param {object} options for encoding. Optional. If not passed, using default in common.
* @returns {string} encode string
*/
countlyCommon.encodeSomeHtml = function(html, options) {
if (options) {
return filterXSS(html, options);
}
else {
return filterXSS(html, htmlEncodeOptions);
}
};
/**
* Calculates the percent change between previous and current values.
* @memberof countlyCommon
* @param {number} previous - data for previous period
* @param {number} current - data for current period
* @returns {object} in the following format {"percent": "20%", "trend": "u"}
* @example
* //outputs {"percent":"100%","trend":"u"}
* countlyCommon.getPercentChange(100, 200);
*/
countlyCommon.getPercentChange = function(previous, current) {
var pChange = 0,
trend = "";
previous = parseFloat(previous);
current = parseFloat(current);
if (previous === 0) {
pChange = "NA";
trend = "u"; //upward
}
else if (current === 0) {
pChange = "∞";
trend = "d"; //downward
}
else {
var change = (((current - previous) / previous) * 100).toFixed(1);
pChange = countlyCommon.getShortNumber(change) + "%";
if (change < 0) {
trend = "d";
}
else {
trend = "u";
}
}
return { "percent": pChange, "trend": trend };
};
/**
* Fetches nested property values from an obj.
* @memberof countlyCommon
* @param {object} obj - standard countly metric object
* @param {string} my_passed_path - dot separate path to fetch from object
* @param {object} def - stub object to return if nothing is found on provided path
* @returns {object} fetched object from provided path
* @example <caption>Path found</caption>
* //outputs {"u":20,"t":20,"n":5}
* countlyCommon.getDescendantProp({"2017":{"1":{"2":{"u":20,"t":20,"n":5}}}}, "2017.1.2", {"u":0,"t":0,"n":0});
* @example <caption>Path not found</caption>
* //outputs {"u":0,"t":0,"n":0}
* countlyCommon.getDescendantProp({"2016":{"1":{"2":{"u":20,"t":20,"n":5}}}}, "2017.1.2", {"u":0,"t":0,"n":0});
*/
countlyCommon.getDescendantProp = function(obj, my_passed_path, def) {
for (var i = 0, my_path = (my_passed_path + "").split('.'), len = my_path.length; i < len; i++) {
if (!obj || typeof obj !== 'object') {
return def;
}
obj = obj[my_path[i]];
}
if (obj === undefined) {
return def;
}
return obj;
};
/**
* Checks if current graph type matches the one being drawn
* @memberof countlyCommon
* @param {string} type - graph type
* @param {object} settings - graph settings
* @returns {boolean} Return true if type is the same
*/
countlyCommon.checkGraphType = function(type, settings) {
var eType = "line";
if (settings && settings.series && settings.series.bars && settings.series.bars.show === true) {
if (settings.series.stack === true) {
eType = "bar";
}
else {
eType = "seperate-bar";
}
}
else if (settings && settings.series && settings.series.pie && settings.series.pie.show === true) {
eType = "pie";
}
if (type === eType) {
return true;
}
else {
return false;
}
};
/**
* Draws a graph with the given dataPoints to container. Used for drawing bar and pie charts.
* @memberof countlyCommon
* @param {object} dataPoints - data poitns to draw on graph
* @param {string|object} container - selector for container or container object itself where to create graph
* @param {string} graphType - type of the graph, accepted values are bar, line, pie, separate-bar
* @param {object} inGraphProperties - object with properties to extend and use on graph library directly
* @returns {boolean} false if container element not found, otherwise true
* @example <caption>Drawing Pie chart</caption>
* countlyCommon.drawGraph({"dp":[
* {"data":[[0,20]],"label":"Test1","color":"#52A3EF"},
* {"data":[[0,30]],"label":"Test2","color":"#FF8700"},
* {"data":[[0,50]],"label":"Test3","color":"#0EC1B9"}
* ]}, "#dashboard-graph", "pie");
* @example <caption>Drawing bar chart, to comapre values with different color bars</caption>
* //[-1,null] and [3,null] are used for offsets from left and right
* countlyCommon.drawGraph({"dp":[
* {"data":[[-1,null],[0,20],[1,30],[2,50],[3,null]],"color":"#52A3EF"}, //first bar set
* {"data":[[-1,null],[0,50],[1,30],[2,20],[3,null]],"color":"#0EC1B9"} //second bar set
*],
* "ticks":[[-1,""],[0,"Test1"],[1,"Test2"],[2,"Test3"],[3,""]]
*}, "#dashboard-graph", "separate-bar", {"series":{"stack":null}});
* @example <caption>Drawing Separate bars chart, to comapre values with different color bars</caption>
* //[-1,null] and [3,null] are used for offsets from left and right
* countlyCommon.drawGraph({"dp":[
* {"data":[[-1,null],[0,20],[1,null],[2,null],[3,null]],"label":"Test1","color":"#52A3EF"},
* {"data":[[-1,null],[0,null],[1,30],[2,null],[3,null]],"label":"Test2","color":"#FF8700"},
* {"data":[[-1,null],[0,null],[1,null],[2,50],[3,null]],"label":"Test3","color":"#0EC1B9"}
*],
* "ticks":[[-1,""],[0,"Test1"],[1,"Test2"],[2,"Test3"],[3,""]
*]}, "#dashboard-graph", "separate-bar");
*/
countlyCommon.drawGraph = function(dataPoints, container, graphType, inGraphProperties) {
var p = 0;
if ($(container).length <= 0) {
return false;
}
if (graphType === "pie") {
var min_treshold = 0.05; //minimum treshold for graph
var break_other = 0.3; //try breaking other in smaller if at least given % from all
var sum = 0;
var i = 0;
var useMerging = true;
for (i = 0; i < dataPoints.dp.length; i++) {
sum = sum + dataPoints.dp[i].data[0][1];
if (dataPoints.dp[i].moreInfo) {
useMerging = false;
}
else {
dataPoints.dp[i].moreInfo = "";
}
}
if (useMerging) {
var dpLength = dataPoints.dp.length;
var treshold_value = Math.round(min_treshold * sum);
var max_other = Math.round(min_treshold * sum);
var under_treshold = [];//array of values under treshold
var left_for_other = sum;
for (i = 0; i < dataPoints.dp.length; i++) {
if (dataPoints.dp[i].data[0][1] >= treshold_value) {
left_for_other = left_for_other - dataPoints.dp[i].data[0][1];
}
else {
under_treshold.push(dataPoints.dp[i].data[0][1]);
}
}
var stop_breaking = Math.round(sum * break_other);
if (left_for_other >= stop_breaking) { //fix values if other takes more than set % of data
under_treshold = under_treshold.sort(function(a, b) {
return a - b;
});
var tresholdMap = [];
treshold_value = treshold_value - 1; //to don't group exactly 5% values later in code
tresholdMap.push({value: treshold_value, text: 5});
var in_this_one = 0;
var count_in_this = 0;
for (p = under_treshold.length - 1; p >= 0 && under_treshold[p] > 0 && left_for_other >= stop_breaking; p--) {
if (under_treshold[p] <= treshold_value) {
if (in_this_one + under_treshold[p] <= max_other || count_in_this < 5) {
count_in_this++;
in_this_one += under_treshold[p];
left_for_other -= under_treshold[p];
}
else {
if (tresholdMap[tresholdMap.length - 1].value === under_treshold[p]) {
in_this_one = 0;
count_in_this = 0;
treshold_value = under_treshold[p] - 1;
}
else {
in_this_one = under_treshold[p];
count_in_this = 1;
treshold_value = under_treshold[p];
left_for_other -= under_treshold[p];
}
tresholdMap.push({value: treshold_value, text: Math.max(0.009, Math.round(treshold_value * 10000 / sum) / 100)});
}
}
}
treshold_value = Math.max(treshold_value - 1, 0);
tresholdMap.push({value: treshold_value, text: Math.round(treshold_value * 10000 / sum) / 100});
var tresholdPointer = 0;
while (tresholdPointer < tresholdMap.length - 1) {
dataPoints.dp.push({"label": tresholdMap[tresholdPointer + 1].text + "-" + tresholdMap[tresholdPointer].text + "%", "data": [[0, 0]], "moreInfo": []});
var tresholdPlace = dataPoints.dp.length - 1;
for (i = 0; i < dpLength; i++) {
if (dataPoints.dp[i].data[0][1] <= tresholdMap[tresholdPointer].value && dataPoints.dp[i].data[0][1] > tresholdMap[tresholdPointer + 1].value) {
dataPoints.dp[tresholdPlace].moreInfo.push({"label": dataPoints.dp[i].label, "value": Math.round(dataPoints.dp[i].data[0][1] * 10000 / sum) / 100});
dataPoints.dp[tresholdPlace].data[0][1] = dataPoints.dp[tresholdPlace].data[0][1] + dataPoints.dp[i].data[0][1];
dataPoints.dp.splice(i, 1);
dpLength = dataPoints.dp.length;
i--;
tresholdPlace--;
}
}
tresholdPointer = tresholdPointer + 1;
}
}
}
}
_.defer(function() {
if ((!dataPoints.dp || !dataPoints.dp.length) || (graphType === "bar" && (!dataPoints.dp[0].data[0] || (typeof dataPoints.dp[0].data[0][1] === 'undefined' && typeof dataPoints.dp[0].data[1][1] === 'undefined') || (dataPoints.dp[0].data[0][1] === null && dataPoints.dp[0].data[1][1] === null)))) {
$(container).hide();
$(container).siblings(".graph-no-data").show();
return true;
}
else {
$(container).show();
$(container).siblings(".graph-no-data").hide();
}
var graphProperties = {
series: {
lines: { show: true, fill: true },
points: { show: true }
},
grid: { hoverable: true, borderColor: "null", color: "#999", borderWidth: 0, minBorderMargin: 10 },
xaxis: { minTickSize: 1, tickDecimals: "number", tickLength: 0 },
yaxis: { min: 0, minTickSize: 1, tickDecimals: "number", position: "right" },
legend: { backgroundOpacity: 0, margin: [20, -19] },
colors: countlyCommon.GRAPH_COLORS
};
switch (graphType) {
case "line":
graphProperties.series = { lines: { show: true, fill: true }, points: { show: true } };
break;
case "bar":
if (dataPoints.ticks.length > 20) {
graphProperties.xaxis.rotateTicks = 45;
}
var barWidth = 0.6;
switch (dataPoints.dp.length) {
case 2:
barWidth = 0.3;
break;
case 3:
barWidth = 0.2;
break;
}
for (i = 0; i < dataPoints.dp.length; i++) {
dataPoints.dp[i].bars = {
order: i,
barWidth: barWidth
};
}
graphProperties.series = { stack: true, bars: { show: true, barWidth: 0.6, tickLength: 0, fill: 1 } };
graphProperties.xaxis.ticks = dataPoints.ticks;
break;
case "separate-bar":
if (dataPoints.ticks.length > 20) {
graphProperties.xaxis.rotateTicks = 45;
}
graphProperties.series = { bars: { show: true, align: "center", barWidth: 0.6, tickLength: 0, fill: 1 } };
graphProperties.xaxis.ticks = dataPoints.ticks;
break;
case "pie":
graphProperties.series = {
pie: {
show: true,
lineWidth: 0,
radius: 115,
innerRadius: 0.45,
combine: {
color: '#CCC',
threshold: 0.05
},
label: {
show: true,
radius: 160
}
}
};
graphProperties.legend.show = false;
break;
default:
break;
}
if (inGraphProperties) {
$.extend(true, graphProperties, inGraphProperties);
}
$.plot($(container), dataPoints.dp, graphProperties);
if (graphType === "bar" || graphType === "separate-bar") {
$(container).unbind("plothover");
$(container).bind("plothover", function(event, pos, item) {
$("#graph-tooltip").remove();
if (item && item.datapoint && item.datapoint[1]) {
// For stacked bar chart calculate the diff
var yAxisValue = item.datapoint[1].toFixed(1).replace(".0", "") - item.datapoint[2].toFixed(1).replace(".0", "");
if (inGraphProperties && inGraphProperties.tooltipType === "duration") {
yAxisValue = countlyCommon.formatSecond(yAxisValue);
}
showTooltip({
x: pos.pageX,
y: item.pageY,
contents: yAxisValue || 0
});
}
});
}
else if (graphType === 'pie') {
$(container).unbind("plothover");
$(container).bind("plothover", function(event, pos, item) {
$("#graph-tooltip").remove();
if (item && item.series && item.series.moreInfo) {
var tooltipcontent;
if (Array.isArray(item.series.moreInfo)) {
tooltipcontent = "<table class='pie_tooltip_table'>";
if (item.series.moreInfo.length <= 5) {
for (p = 0; p < item.series.moreInfo.length; p++) {
tooltipcontent = tooltipcontent + "<tr><td>" + item.series.moreInfo[p].label + ":</td><td>" + item.series.moreInfo[p].value + "%</td>";
}
}
else {
for (p = 0; p < 5; p = p + 1) {
tooltipcontent += "<tr><td>" + item.series.moreInfo[p].label + " :</td><td>" + item.series.moreInfo[p].value + "%</td></tr>";
}
tooltipcontent += "<tr><td colspan='2' style='text-align:center;'>...</td></tr><tr><td style='text-align:center;' colspan=2>(and " + (item.series.moreInfo.length - 5) + " other)</td></tr>";
}
tooltipcontent += "</table>";
}
else {
tooltipcontent = item.series.moreInfo;
}
showTooltip({
x: pos.pageX,
y: pos.pageY,
contents: tooltipcontent
});
}
});
}
else {
$(container).unbind("plothover");
}
}, dataPoints, container, graphType, inGraphProperties);
return true;
};
/**
* Draws a time line graph with the given dataPoints to container.
* @memberof countlyCommon
* @param {object} dataPoints - data points to draw on graph
* @param {string|object} container - selector for container or container object itself where to create graph
* @param {string=} bucket - time bucket to display on graph. See {@link countlyCommon.getTickObj}
* @param {string=} overrideBucket - time bucket to display on graph. See {@link countlyCommon.getTickObj}
* @param {boolean=} small - if graph won't be full width graph
* @param {array=} appIdsForNotes - display notes from provided apps ids on graph, will not show notes when empty
* @param {object=} options - extra graph options, see flot documentation
* @example
* countlyCommon.drawTimeGraph([{
* "data":[[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,12],[8,9],[9,10],[10,5],[11,8],[12,7],[13,9],[14,4],[15,6]],
* "label":"Total Sessions",
* "color":"#DDDDDD",
* "mode":"ghost"
*},{
* "data":[[1,74],[2,69],[3,60],[4,17],[5,6],[6,3],[7,13],[8,25],[9,62],[10,34],[11,34],[12,33],[13,34],[14,30],[15,1]],
* "label":"Total Sessions",
* "color":"#333933"
*}], "#dashboard-graph");
*/
countlyCommon.drawTimeGraph = function(dataPoints, container, bucket, overrideBucket, small, appIdsForNotes, options) {
_.defer(function() {
if (!dataPoints || !dataPoints.length) {
$(container).hide();
$(container).siblings(".graph-no-data").show();
return true;
}
else {
$(container).show();
$(container).siblings(".graph-no-data").hide();
}
var i = 0;
var j = 0;
// Some data points start with [1, XXX] (should be [0, XXX]) and brakes the new tick logic
// Below loops converts the old structures to the new one
if (dataPoints[0].data[0][0] === 1) {
for (i = 0; i < dataPoints.length; i++) {
for (j = 0; j < dataPoints[i].data.length; j++) {
dataPoints[i].data[j][0] -= 1;
}
}
}
var minValue = dataPoints[0].data[0][1];
var maxValue = dataPoints[0].data[0][1];
for (i = 0; i < dataPoints.length; i++) {
for (j = 0; j < dataPoints[i].data.length; j++) {
dataPoints[i].data[j][1] = Math.round(dataPoints[i].data[j][1] * 1000) / 1000; // 3 decimal places max
if (dataPoints[i].data[j][1] < minValue) {
minValue = dataPoints[i].data[j][1];
}
if (dataPoints[i].data[j][1] > maxValue) {
maxValue = dataPoints[i].data[j][1];
}
}
}
var myTickDecimals = 0;
var myMinTickSize = 1;
if (maxValue < 1 && maxValue > 0) {
myTickDecimals = maxValue.toString().length - 2;
myMinTickSize = 0.001;
}
var graphProperties = {
series: {
lines: {
stack: false,
show: false,
fill: true,
lineWidth: 2.5,
fillColor: {
colors: [
{ opacity: 0 },
{ opacity: 0 }
]
},
shadowSize: 0
},
splines: {
show: true,
lineWidth: 2.5
},
points: { show: true, radius: 0, shadowSize: 0, lineWidth: 2 },
shadowSize: 0
},
crosshair: { mode: "x", color: "rgba(78,78,78,0.4)" },
grid: { hoverable: true, borderColor: "null", color: "#666", borderWidth: 0, minBorderMargin: 10, labelMargin: 10 },
xaxis: { tickDecimals: "number", tickSize: 0, tickLength: 0 },
yaxis: { min: 0, minTickSize: 1, tickDecimals: "number", ticks: 3, position: "right"},
legend: { show: false, margin: [-25, -44], noColumns: 3, backgroundOpacity: 0 },
colors: countlyCommon.GRAPH_COLORS,
};
//overriding values
graphProperties.yaxis.minTickSize = myMinTickSize;
graphProperties.yaxis.tickDecimals = myTickDecimals;
if (myMinTickSize < 1) {
graphProperties.yaxis.tickFormatter = function(number) {
return (Math.round(number * 1000) / 1000).toString();
};
}
graphProperties.series.points.show = (dataPoints[0].data.length <= 90);
if (overrideBucket) {
graphProperties.series.points.radius = 4;
}
var graphTicks = [],
tickObj = {};
if (_period === "month" && !bucket) {
tickObj = countlyCommon.getTickObj("monthly");
if (tickObj.labelCn === 1) {
for (var kk = 0; kk < dataPoints.length; kk++) {
dataPoints[kk].data = dataPoints[kk].data.slice(0, 1);
}
graphProperties.series.points.radius = 4;
overrideBucket = true;//to get the dots added
}
else if (tickObj.labelCn === 2) {
for (var kkk = 0; kkk < dataPoints.length; kkk++) {
dataPoints[kkk].data = dataPoints[kkk].data.slice(0, 2);
}
}
}
else {
tickObj = countlyCommon.getTickObj(bucket, overrideBucket);
}
if (small) {
for (i = 0; i < tickObj.ticks.length; i = i + 2) {
tickObj.ticks[i][1] = "";
}
graphProperties.xaxis.font = {
size: 11,
color: "#a2a2a2"
};
}
graphProperties.xaxis.max = tickObj.max;
graphProperties.xaxis.min = tickObj.min;
graphProperties.xaxis.ticks = tickObj.ticks;
graphTicks = tickObj.tickTexts;
//set dashed line for not finished yet
if (countlyCommon.periodObj.periodContainsToday === true) {
var settings = countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID];
var tzDate = new Date(new Date().toLocaleString('en-US', { timeZone: settings.timezone }));
for (var z = 0; z < dataPoints.length; z++) {
if (dataPoints[z].mode !== "ghost" && dataPoints[z].mode !== "previous") {
var bDate = new Date();
if (_period === "hour") {
if (bDate.getDate() === tzDate.getDate()) {
dataPoints[z].dashAfter = tzDate.getHours() - 1;
}
else if (bDate.getDate() > tzDate.getDate()) {
dataPoints[z].dashed = true; //all dashed because app lives still in yesterday
}
//for last - none dashed - because app lives in tomorrow(so don't do anything for this case)
}
else if (_period === "day") { //days in this month
var c = countlyCommon.periodObj.currentPeriodArr.length;
dataPoints[z].dashAfter = c - 2;
}
else if (_period === "month" && bDate.getMonth() <= 2 && (!bucket || bucket === "monthly")) {
dataPoints[z].dashed = true;
}
else {
if (bucket === "hourly") {
dataPoints[z].dashAfter = graphTicks.length - (24 - tzDate.getHours() + 1);
}
else {
dataPoints[z].dashAfter = graphTicks.length - 2;
}
}
if (typeof dataPoints[z].dashAfter !== 'undefined' && dataPoints[z].dashAfter <= 0) {
delete dataPoints[z].dashAfter;
dataPoints[z].dashed = true; //dash whole line
}
}
}
}
var graphObj = $(container).data("plot"),
keyEventCounter = "A",
keyEvents = [];
//keyEventsIndex = 0;
if (!(options && _.isObject(options) && $(container).parents("#dashboard-data").length > 0)) {
countlyCommon.deepObjectExtend(graphProperties, {
series: {lines: {show: true}, splines: {show: false}},
zoom: {active: true},
pan: {interactive: true, active: true, mode: "smartLock", frameRate: 120},
xaxis: {zoomRange: false, panRange: false},
yaxis: {showZeroTick: true, ticks: 5}
});
}
if (options && _.isObject(options)) {
countlyCommon.deepObjectExtend(graphProperties, options);
}
if (graphObj && countlyCommon.checkGraphType("line", graphObj.getOptions()) && graphObj.getOptions().series && graphObj.getOptions().grid.show && graphObj.getOptions().series.splines && graphObj.getOptions().yaxis.minTickSize === graphProperties.yaxis.minTickSize) {
graphObj = $(container).data("plot");
if (overrideBucket) {
graphObj.getOptions().series.points.radius = 4;
}
else {
graphObj.getOptions().series.points.radius = 0;
}
graphObj.getOptions().xaxes[0].max = tickObj.max;
graphObj.getOptions().xaxes[0].min = tickObj.min;
graphObj.getOptions().xaxes[0].ticks = tickObj.ticks;
graphObj.setData(dataPoints);
graphObj.setupGrid();
graphObj.draw();
}
else {
graphObj = $.plot($(container), dataPoints, graphProperties);
}
/** function calculates min and max
* @param {number} index - index
* @param {object} el - element
* @returns {boolean} true(if not set), else return nothing
*/
var findMinMax = function(index, el) {
// data point is null, this workaround is used to start drawing graph with a certain padding
if (!el[1] && parseInt(el[1]) !== 0) {
return true;
}
el[1] = parseFloat(el[1]);
if (el[1] >= tmpMax) {
tmpMax = el[1];
tmpMaxIndex = el[0];
}
if (el[1] <= tmpMin) {
tmpMin = el[1];
tmpMinIndex = el[0];
}
};
var k = 0;
for (k = 0; k < graphObj.getData().length; k++) {
var tmpMax = 0,
tmpMaxIndex = 0,
tmpMin = 999999999999,
tmpMinIndex = 0,
label = (graphObj.getData()[k].label + "").toLowerCase();
if (graphObj.getData()[k].mode === "ghost") {
//keyEventsIndex += graphObj.getData()[k].data.length;
continue;
}
$.each(graphObj.getData()[k].data, findMinMax);
if (tmpMax === tmpMin) {
continue;
}
keyEvents[k] = [];
keyEvents[k][keyEvents[k].length] = {
data: [tmpMinIndex, tmpMin],
code: keyEventCounter,
color: graphObj.getData()[k].color,
event: "min",
desc: jQuery.i18n.prop('common.graph-min', tmpMin, label, graphTicks[tmpMinIndex])
};
keyEventCounter = String.fromCharCode(keyEventCounter.charCodeAt() + 1);
keyEvents[k][keyEvents[k].length] = {
data: [tmpMaxIndex, tmpMax],
code: keyEventCounter,
color: graphObj.getData()[k].color,
event: "max",
desc: jQuery.i18n.prop('common.graph-max', tmpMax, label, graphTicks[tmpMaxIndex])
};
keyEventCounter = String.fromCharCode(keyEventCounter.charCodeAt() + 1);
}
var drawLabels = function() {
var graphWidth = graphObj.width(),
graphHeight = graphObj.height();
$(container).find(".graph-key-event-label").remove();
$(container).find(".graph-note-label").remove();
for (k = 0; k < keyEvents.length; k++) {
var bgColor = graphObj.getData()[k].color;
if (!keyEvents[k]) {
continue;
}
for (var l = 0; l < keyEvents[k].length; l++) {
var o = graphObj.pointOffset({ x: keyEvents[k][l].data[0], y: keyEvents[k][l].data[1] });
if (o.top <= 40) {
o.top = 40;
}
else if (o.top >= (graphHeight + 30)) {
o.top = graphHeight + 30;
}
if (o.left <= 15) {
o.left = 15;
}
else if (o.left >= (graphWidth - 15)) {
o.left = (graphWidth - 15);
}
var keyEventLabel = $('<div class="graph-key-event-label">').text(keyEvents[k][l].code);
keyEventLabel.attr({
"title": keyEvents[k][l].desc,
"data-points": "[" + keyEvents[k][l].data + "]"
}).css({
"position": 'absolute',
"left": o.left,
"top": o.top - 33,
"display": 'none',
"background-color": bgColor
}).appendTo(graphObj.getPlaceholder()).show();
$(".tipsy").remove();
keyEventLabel.tipsy({ gravity: $.fn.tipsy.autoWE, offset: 3, html: true });
}
}
// Add note labels to the graph
if (appIdsForNotes && !(countlyGlobal && countlyGlobal.ssr) && !(bucket === "hourly" && dataPoints[0].data.length > 24) && bucket !== "weekly") {
var noteDateIds = countlyCommon.getNoteDateIds(bucket),
frontData = graphObj.getData()[graphObj.getData().length - 1],
startIndex = (!frontData.data[1] && frontData.data[1] !== 0) ? 1 : 0;
for (k = 0, l = startIndex; k < frontData.data.length; k++, l++) {
if (frontData.data[l]) {
var graphPoint = graphObj.pointOffset({ x: frontData.data[l][0], y: frontData.data[l][1] });
var notes = countlyCommon.getNotesForDateId(noteDateIds[k], appIdsForNotes);
var colors = ["#79a3e9", "#70bbb8", "#e2bc33", "#a786cd", "#dd6b67", "#ece176"];
if (notes.length) {
var labelColor = colors[notes[0].color - 1];
var titleDom = '';
if (notes.length === 1) {
var noteTime = moment(notes[0].ts).format("D MMM, HH:mm");
var noteId = notes[0].app_id;
var app = countlyGlobal.apps[noteId] || {};
titleDom = "<div> <div class='note-header'><div class='note-title'>" + noteTime + "</div><div class='note-app' style='display:flex;line-height: 15px;'> <div class='icon' style='display:inline-block; border-radius:2px; width:15px; height:15px; margin-right: 5px; background: url(appimages/" + noteId + ".png) center center / cover no-repeat;'></div><span>" + app.name + "</span></div></div>" +
"<div class='note-content'>" + notes[0].note + "</div>" +
"<div class='note-footer'> <span class='note-owner'>" + (notes[0].owner_name) + "</span> | <span class='note-type'>" + (jQuery.i18n.map["notes.note-" + notes[0].noteType] || notes[0].noteType) + "</span> </div>" +
"</div>";
}
else {
var noteDateFormat = "D MMM, YYYY";
if (countlyCommon.getPeriod() === "month") {
noteDateFormat = "MMM YYYY";
}
noteTime = moment(notes[0].ts).format(noteDateFormat);
titleDom = "<div><div class='note-header'><div class='note-title'>" + noteTime + "</div></div>" +
"<div class='note-content'><span onclick='countlyCommon.getNotesPopup(" + noteDateIds[k] + "," + JSON.stringify(appIdsForNotes) + ")' class='notes-view-link'>View Notes (" + notes.length + ")</span></div>" +
"</div>";
}
var graphNoteLabel = $('<div class="graph-note-label graph-text-note" style="background-color:' + labelColor + ';"><div class="fa fa-align-left" ></div></div>');
graphNoteLabel.attr({
"title": titleDom,
"data-points": "[" + frontData.data[l] + "]"
}).css({
"position": 'absolute',
"left": graphPoint.left,
"top": graphPoint.top - 53,
"display": 'none',
"border-color": frontData.color
}).appendTo(graphObj.getPlaceholder()).show();
$(".tipsy").remove();
graphNoteLabel.tipsy({cssClass: 'tipsy-for-note', gravity: $.fn.tipsy.autoWE, offset: 3, html: true, trigger: 'hover', hoverable: true });
}
}
}
}
};
drawLabels();
$(container).on("mouseout", function() {
graphObj.unlockCrosshair();
graphObj.clearCrosshair();
graphObj.unhighlight();
$("#graph-tooltip").fadeOut(200, function() {
$(this).remove();
});
});
/** dShows tooltip
* @param {number} dataIndex - index
* @param {object} position - position
* @param {boolean} onPoint - if point found
*/
function showCrosshairTooltip(dataIndex, position, onPoint) {
//increase dataIndex if ticks are padded
var tickIndex = dataIndex;
if ((tickObj.ticks && tickObj.ticks[0] && tickObj.ticks[0][0] < 0) && (tickObj.tickTexts && tickObj.tickTexts[0] === "")) {
tickIndex++;
}
var tooltip = $("#graph-tooltip");
var crossHairPos = graphObj.p2c(position);
var minpoz = Math.max(200, tooltip.width());
var tooltipLeft = (crossHairPos.left < minpoz) ? crossHairPos.left + 20 : crossHairPos.left - tooltip.width() - 20;
tooltip.css({ left: tooltipLeft });
if (onPoint) {
var dataSet = graphObj.getData(),
tooltipHTML = "<div class='title'>" + tickObj.tickTexts[tickIndex] + "</div>";
dataSet = _.sortBy(dataSet, function(obj) {
return obj.data[dataIndex][1];
});
for (var m = dataSet.length - 1; m >= 0; --m) {
var series = dataSet[m],
formattedValue = series.data[dataIndex][1];
var addMe = "";
// Change label to previous period if there is a ghost graph
if (series.mode === "ghost") {
series.label = jQuery.i18n.map["common.previous-period"];
}
var opacity = "1.0";
//add lines over color block for dashed
if (series.dashed && series.previous) {
addMe = '<svg style="width: 12px; height: 12px; position:absolute; top:0; left:0;"><line stroke-dasharray="2, 2" x1="0" y1="100%" x2="100%" y2="0" style="stroke:rgb(255,255,255);stroke-width:30"/></svg>';
}
if (series.alpha) {
opacity = series.alpha + "";
}
if (formattedValue) {
formattedValue = parseFloat(formattedValue).toFixed(2).replace(/[.,]00$/, "");
}
if (series.data[dataIndex][2]) {
formattedValue = series.data[dataIndex][2]; // to show customized string value tips
}
tooltipHTML += "<div class='inner'>";
tooltipHTML += "<div class='color' style='position:relative; background-color: " + series.color + "; opacity:" + opacity + ";'>" + addMe + "</div>";
tooltipHTML += "<div class='series'>" + series.label + "</div>";
tooltipHTML += "<div class='value'>" + formattedValue + "</div>";
tooltipHTML += "</div>";
}
if (tooltip.length) {
tooltip.html(tooltipHTML);
}
else {
tooltip = $("<div id='graph-tooltip' class='white' style='top:-15px;'>" + tooltipHTML + "</div>");
$(container).prepend(tooltip);
}
if (tooltip.is(":visible")) {
tooltip.css({
"transition": "left .15s"
});
}
else {
tooltip.fadeIn();
}
}
}
$(container).unbind("plothover");
$(container).bind("plothover", function(event, pos) {
graphObj.unlockCrosshair();
graphObj.unhighlight();
var dataset = graphObj.getData(),
pointFound = false;
for (i = 0; i < dataset.length; ++i) {
var series = dataset[i];
// Find the nearest points, x-wise
for (j = 0; j < series.data.length; ++j) {
var currX = series.data[j][0],
currCrossX = pos.x.toFixed(2);
if ((currX - 0.10) < currCrossX && (currX + 0.10) > currCrossX) {
graphObj.lockCrosshair({
x: series.data[j][0],
y: series.data[j][1]
});
graphObj.highlight(series, j);
pointFound = true;
break;
}
}
}
showCrosshairTooltip(j, pos, pointFound);
});
if (!(options && _.isObject(options) && $(container).parents("#dashboard-data").length > 0)) {
var zoomTarget = $(container),
zoomContainer = $(container).parent();
zoomContainer.find(".zoom").remove();
zoomContainer.prepend("<div class=\"zoom\"><div class=\"zoom-button zoom-out\"></div><div class=\"zoom-button zoom-reset\"></div><div class=\"zoom-button zoom-in\"></div></div>");
zoomTarget.addClass("pannable");
zoomContainer.data("zoom", zoomContainer.data("zoom") || 1);
zoomContainer.find(".zoom-in").tooltipster({
theme: "tooltipster-borderless",
content: $.i18n.map["common.zoom-in"]
});
zoomContainer.find(".zoom-out").tooltipster({
theme: "tooltipster-borderless",
content: $.i18n.map["common.zoom-out"]
});
zoomContainer.find(".zoom-reset").tooltipster({
theme: "tooltipster-borderless",
content: {},
functionFormat: function() {
return $.i18n.prop("common.zoom-reset", Math.round(zoomContainer.data("zoom") * 100));
}
});
zoomContainer.find(".zoom-out").off("click").on("click", function() {
var plot = zoomTarget.data("plot");
plot.zoomOut({
amount: 1.5,
center: {left: plot.width() / 2, top: plot.height()}
});
zoomContainer.data("zoom", zoomContainer.data("zoom") / 1.5);
});
zoomContainer.find(".zoom-reset").off("click").on("click", function() {
var plot = zoomTarget.data("plot");
plot.zoomOut({
amount: zoomContainer.data("zoom"),
center: {left: plot.width() / 2, top: plot.height()}
});
zoomContainer.data("zoom", 1);
var yaxis = plot.getAxes().yaxis;
var panOffset = yaxis.p2c(0) - plot.height() + 2;
if (Math.abs(panOffset) > 2) {
plot.pan({top: panOffset});
}
});
zoomContainer.find(".zoom-in").off("click").on("click", function() {
var plot = zoomTarget.data("plot");
plot.zoom({
amount: 1.5,
center: {left: plot.width() / 2, top: plot.height()}
});
zoomContainer.data("zoom", zoomContainer.data("zoom") * 1.5);
});
zoomTarget.off("plotzoom").on("plotzoom", function() {
drawLabels();
});
zoomTarget.off("plotpan").on("plotpan", function() {
drawLabels();
});
}
}, dataPoints, container, bucket);
};
/**
* Draws a gauge with provided value on procided container.
* @memberof countlyCommon
* @param {string|object} targetEl - selector for container or container object itself where to create graph
* @param {number} value - value to display on gauge
* @param {number} maxValue - maximal value of the gauge
* @param {string} gaugeColor - color of the gauge in hexadecimal string as #ffffff
* @param {string|object} textField - selector for container or container object itself where to output textual value
*/
countlyCommon.drawGauge = function(targetEl, value, maxValue, gaugeColor, textField) {
var opts = {
lines: 12,
angle: 0.15,
lineWidth: 0.44,
pointer: {
length: 0.7,
strokeWidth: 0.05,
color: '#000000'
},
colorStart: gaugeColor,
colorStop: gaugeColor,
strokeColor: '#E0E0E0',
generateGradient: true
};
var gauge = new Gauge($(targetEl)[0]).setOptions(opts);
if (textField) {
gauge.setTextField($(textField)[0]);
}
gauge.maxValue = maxValue;
gauge.set(1);
gauge.set(value);
};
/**
* Draws horizibtally stacked bars like in platforms and density analytic sections.
* @memberof countlyCommon
* @param {array} data - data to draw in form of [{"data":[[0,85]],"label":"Test1"},{"data":[[0,79]],"label":"Test2"},{"data":[[0,78]],"label":"Test3"}]
* @param {object|string} intoElement - selector for container or container object itself where to create graph
* @param {number} colorIndex - index of color from {@link countlyCommon.GRAPH_COLORS}
*/
countlyCommon.drawHorizontalStackedBars = function(data, intoElement, colorIndex) {
var processedData = [],
tmpProcessedData = [],
totalCount = 0,
maxToDisplay = 10,
barHeight = 30;
var i = 0;
for (i = 0; i < data.length; i++) {
tmpProcessedData.push({
label: data[i].label,
count: data[i].data[0][1],
index: i
});
totalCount += data[i].data[0][1];
}
var totalPerc = 0,
proCount = 0;
for (i = 0; i < tmpProcessedData.length; i++) {
if (i >= maxToDisplay) {
processedData.push({
label: "Other",
count: totalCount - proCount,
perc: countlyCommon.round((100 - totalPerc), 2) + "%",
index: i
});
break;
}
var perc = countlyCommon.round((tmpProcessedData[i].count / totalCount) * 100, 2);
tmpProcessedData[i].perc = perc + "%";
totalPerc += perc;
proCount += tmpProcessedData[i].count;
processedData.push(tmpProcessedData[i]);
}
if (processedData.length > 0) {
var percentSoFar = 0;
var chart = d3.select(intoElement)
.attr("width", "100%")
.attr("height", barHeight);
var bar = chart.selectAll("g")
.data(processedData)
.enter().append("g");
bar.append("rect")
.attr("width", function(d) {
return ((d.count / totalCount) * 100) + "%";
})
.attr("x", function(d) {
var myPercent = percentSoFar;
percentSoFar = percentSoFar + (100 * (d.count / totalCount));
return myPercent + "%";
})
.attr("height", barHeight)
.attr("fill", function(d) {
if (colorIndex || colorIndex === 0) {
return countlyCommon.GRAPH_COLORS[colorIndex];
}
else {
return countlyCommon.GRAPH_COLORS[d.index];
}
})
.attr("stroke", "#FFF")
.attr("stroke-width", 2);
if (colorIndex || colorIndex === 0) {
bar.attr("opacity", function(d) {
return 1 - (0.05 * d.index);
});
}
percentSoFar = 0;
bar.append("foreignObject")
.attr("width", function(d) {
return ((d.count / totalCount) * 100) + "%";
})
.attr("height", barHeight)
.attr("x", function(d) {
var myPercent = percentSoFar;
percentSoFar = percentSoFar + (100 * (d.count / totalCount));
return myPercent + "%";
})
.append("xhtml:div")
.attr("class", "hsb-tip")
.html(function(d) {
return "<div>" + d.perc + "</div>";
});
percentSoFar = 0;
bar.append("text")
.attr("x", function(d) {
var myPercent = percentSoFar;
percentSoFar = percentSoFar + (100 * (d.count / totalCount));
return myPercent + 0.5 + "%";
})
.attr("dy", "1.35em")
.text(function(d) {
return d.label;
});
}
else {
var chart1 = d3.select(intoElement)
.attr("width", "100%")
.attr("height", barHeight);
var bar1 = chart1.selectAll("g")
.data([{ text: jQuery.i18n.map["common.bar.no-data"] }])
.enter().append("g");
bar1.append("rect")
.attr("width", "100%")
.attr("height", barHeight)
.attr("fill", "#FBFBFB")
.attr("stroke", "#FFF")
.attr("stroke-width", 2);
bar1.append("foreignObject")
.attr("width", "100%")
.attr("height", barHeight)
.append("xhtml:div")
.attr("class", "no-data")
.html(function(d) {
return d.text;
});
}
};
/**
* Extract range data from standard countly metric data model
* @memberof countlyCommon
* @param {object} db - countly standard metric data object
* @param {string} propertyName - name of the property to extract
* @param {object} rangeArray - array of all metrics/segments to extract (usually what is contained in meta)
* @param {function} explainRange - function to convert range/bucket index to meaningful label
* @param {array} myorder - arrays of preferred order for give keys. Optional. If not passed - sorted by values
* @returns {array} array containing extracted ranged data as [{"f":"First session","t":352,"percent":"88.4"},{"f":"2 days","t":46,"percent":"11.6"}]
* @example <caption>Extracting session frequency from users collection</caption>
* //outputs [{"f":"First session","t":352,"percent":"88.4"},{"f":"2 days","t":46,"percent":"11.6"}]
* countlyCommon.extractRangeData(_userDb, "f", _frequencies, countlySession.explainFrequencyRange);
*/
countlyCommon.extractRangeData = function(db, propertyName, rangeArray, explainRange, myorder) {
countlyCommon.periodObj = getPeriodObj();
var dataArr = [],
dataArrCounter = 0,
rangeTotal,
total = 0;
var tmp_x = 0;
if (!rangeArray) {
return dataArr;
}
for (var j = 0; j < rangeArray.length; j++) {
rangeTotal = 0;
if (!countlyCommon.periodObj.isSpecialPeriod) {
tmp_x = countlyCommon.getDescendantProp(db, countlyCommon.periodObj.activePeriod + "." + propertyName);
if (tmp_x && tmp_x[rangeArray[j]]) {
rangeTotal += tmp_x[rangeArray[j]];
}
if (rangeTotal !== 0) {
dataArr[dataArrCounter] = {};
dataArr[dataArrCounter][propertyName] = (explainRange) ? explainRange(rangeArray[j]) : rangeArray[j];
dataArr[dataArrCounter].t = rangeTotal;
total += rangeTotal;
dataArrCounter++;
}
}
else {
var tmpRangeTotal = 0;
var i = 0;
for (i = 0; i < (countlyCommon.periodObj.uniquePeriodArr.length); i++) {
tmp_x = countlyCommon.getDescendantProp(db, countlyCommon.periodObj.uniquePeriodArr[i] + "." + propertyName);
if (tmp_x && tmp_x[rangeArray[j]]) {
rangeTotal += tmp_x[rangeArray[j]];
}
}
for (i = 0; i < (countlyCommon.periodObj.uniquePeriodCheckArr.length); i++) {
tmp_x = countlyCommon.getDescendantProp(db, countlyCommon.periodObj.uniquePeriodCheckArr[i] + "." + propertyName);
if (tmp_x && tmp_x[rangeArray[j]]) {
tmpRangeTotal += tmp_x[rangeArray[j]];
}
}
if (rangeTotal > tmpRangeTotal) {
rangeTotal = tmpRangeTotal;
}
if (rangeTotal !== 0) {
dataArr[dataArrCounter] = {};
dataArr[dataArrCounter][propertyName] = (explainRange) ? explainRange(rangeArray[j]) : rangeArray[j];
dataArr[dataArrCounter].t = rangeTotal;
total += rangeTotal;
dataArrCounter++;
}
}
}
for (var z = 0; z < dataArr.length; z++) {
dataArr[z].percent = ((dataArr[z].t / total) * 100).toFixed(1);
}
if (myorder && Array.isArray(myorder)) {
dataArr.sort(function(a, b) {
return (myorder.indexOf(a[propertyName]) - myorder.indexOf(b[propertyName]));
});
}
else {
dataArr.sort(function(a, b) {
return -(a.t - b.t);
});
}
return dataArr;
};
/**
* Extract single level data without metrics/segments, like total user data from users collection
* @memberof countlyCommon
* @param {object} db - countly standard metric data object
* @param {function} clearFunction - function to prefill all expected properties as u, t, n, etc with 0, so you would not have null in the result which won't work when drawing graphs
* @param {object} chartData - prefill chart data with labels, colors, etc
* @param {object} dataProperties - describing which properties and how to extract
* @param {string} metric - metric to select
* @param {boolean} disableHours - disable hourly data for graphs
* @returns {object} object to use in timeline graph with {"chartDP":chartData, "chartData":_.compact(tableData), "keyEvents":keyEvents}
* @example <caption>Extracting total users data from users collection</caption>
* countlyCommon.extractChartData(_sessionDb, countlySession.clearObject, [
* { data:[], label:"Total Users" }
* ], [
* {
* name:"t",
* func:function (dataObj) {
* return dataObj["u"]
* }
* }
* ]);
* @example <caption>Returned data</caption>
* {"chartDP":[
* {
* "data":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],[8,0],[9,0],[10,0],[11,0],[12,0],[13,0],[14,0],[15,12]],
* "label":"Total Sessions",
* "color":"#DDDDDD",
* "mode":"ghost"
* },
* {
* "data":[[0,6],[1,14],[2,11],[3,18],[4,10],[5,32],[6,53],[7,55],[8,71],[9,82],[10,74],[11,69],[12,60],[13,17],[14,6],[15,3]],
* "label":"Total Sessions",
* "color":"#333933"
* }
* ],
* "chartData":[
* {"date":"22 Dec, 2016","pt":0,"t":6},
* {"date":"23 Dec, 2016","pt":0,"t":14},
* {"date":"24 Dec, 2016","pt":0,"t":11},
* {"date":"25 Dec, 2016","pt":0,"t":18},
* {"date":"26 Dec, 2016","pt":0,"t":10},
* {"date":"27 Dec, 2016","pt":0,"t":32},
* {"date":"28 Dec, 2016","pt":0,"t":53},
* {"date":"29 Dec, 2016","pt":0,"t":55},
* {"date":"30 Dec, 2016","pt":0,"t":71},
* {"date":"31 Dec, 2016","pt":0,"t":82},
* {"date":"1 Jan, 2017","pt":0,"t":74},
* {"date":"2 Jan, 2017","pt":0,"t":69},
* {"date":"3 Jan, 2017","pt":0,"t":60},
* {"date":"4 Jan, 2017","pt":0,"t":17},
* {"date":"5 Jan, 2017","pt":0,"t":6},
* {"date":"6 Jan, 2017","pt":12,"t":3}
* ],
* "keyEvents":[{"min":0,"max":12},{"min":0,"max":82}]
* }
*/
countlyCommon.extractChartData = function(db, clearFunction, chartData, dataProperties, metric, disableHours) {
if (metric) {
metric = "." + metric;
}
else {
metric = "";
}
countlyCommon.periodObj = getPeriodObj();
var periodMin = countlyCommon.periodObj.periodMin,
periodMax = (countlyCommon.periodObj.periodMax + 1),
dataObj = {},
formattedDate = "",
tableData = [],
propertyNames = _.pluck(dataProperties, "name"),
propertyFunctions = _.pluck(dataProperties, "func"),
currOrPrevious = _.pluck(dataProperties, "period"),
activeDate,
activeDateArr,
dateString = countlyCommon.periodObj.dateString;
var previousDateArr = [];
if (countlyCommon.periodObj.daysInPeriod === 1 && disableHours) {
periodMax = 1;
dateString = "D MMM";
}
for (var j = 0; j < propertyNames.length; j++) {
if (currOrPrevious[j] === "previous") {
if (countlyCommon.periodObj.daysInPeriod === 1 && !disableHours) {
periodMin = 0;
periodMax = 24;
activeDate = countlyCommon.periodObj.previousPeriodArr[0];
}
else {
if (countlyCommon.periodObj.isSpecialPeriod) {
periodMin = 0;
periodMax = countlyCommon.periodObj.previousPeriodArr.length;
activeDateArr = countlyCommon.periodObj.previousPeriodArr;
}
else {
activeDate = countlyCommon.periodObj.previousPeriod;
activeDateArr = countlyCommon.periodObj.previousPeriodArr;
}
}
}
else if (currOrPrevious[j] === "previousThisMonth") {
//get first date of current month
var date = new Date();
var lastDay = new Date(date.getFullYear(), date.getMonth(), 1);
// count of days of the current month
var currentMonthCount = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
// get date of currenthMonthCount days ago
var firstDay = new Date(lastDay.getTime());
firstDay.setDate(lastDay.getDate() - currentMonthCount);
// get an array between two date
for (var arr = [], dt = new Date(firstDay); dt <= new Date(lastDay); dt.setDate(dt.getDate() + 1)) {
arr.push(dt.getFullYear() + '.' + (dt.getMonth() + 1) + '.' + dt.getDate());
}
previousDateArr = arr;
}
else {
if (countlyCommon.periodObj.isSpecialPeriod) {
if (countlyCommon.periodObj.isHourly) {
periodMin = 0;
periodMax = countlyCommon.periodObj.currentPeriodArr.length;
activeDateArr = countlyCommon.periodObj.currentPeriodArr;
}
else if (countlyCommon.periodObj.daysInPeriod === 1 && !disableHours) {
periodMin = 0;
periodMax = 24;
activeDate = countlyCommon.periodObj.currentPeriodArr[0];
}
else {
periodMin = 0;
periodMax = countlyCommon.periodObj.currentPeriodArr.length;
activeDateArr = countlyCommon.periodObj.currentPeriodArr;
}
}
else {
activeDate = countlyCommon.periodObj.activePeriod;
activeDateArr = countlyCommon.periodObj.currentPeriodArr;
}
}
if (currOrPrevious[j] === "previousThisMonth") {
for (var p = 0, counter_ = 0; p < previousDateArr.length - 1; p++, counter_++) {
formattedDate = moment((previousDateArr[p]).replace(/\./g, "/"), "YYYY/MM/DD");
dataObj = countlyCommon.getDescendantProp(db, previousDateArr[p] + metric);
dataObj = clearFunction(dataObj);
if (!tableData[counter_]) {
tableData[counter_] = {};
}
tableData[counter_].date = countlyCommon.formatDate(formattedDate, dateString);
var propertyValue_ = "";
if (propertyFunctions[j]) {
propertyValue_ = propertyFunctions[j](dataObj);
}
else {
propertyValue_ = dataObj[propertyNames[j]];
}
chartData[j].data[chartData[j].data.length] = [counter_, propertyValue_];
tableData[counter_][propertyNames[j]] = propertyValue_;
}
}
else {
for (var i = periodMin, counter = 0; i < periodMax; i++, counter++) {
if ((!countlyCommon.periodObj.isSpecialPeriod && !disableHours) || (!countlyCommon.periodObj.isSpecialPeriod && disableHours && countlyCommon.periodObj.daysInPeriod !== 1)) {
if (countlyCommon.periodObj.periodMin === 0) {
formattedDate = moment((activeDate + " " + i + ":00:00").replace(/\./g, "/"), "YYYY/MM/DD HH:mm:ss");
}
else if (("" + activeDate).indexOf(".") === -1) {
formattedDate = moment((activeDate + "/" + i + "/1").replace(/\./g, "/"), "YYYY/MM/DD");
}
else {
formattedDate = moment((activeDate + "/" + i).replace(/\./g, "/"), "YYYY/MM/DD");
}
dataObj = countlyCommon.getDescendantProp(db, activeDate + "." + i + metric);
}
else if (countlyCommon.periodObj.isHourly) {
formattedDate = moment((activeDateArr[i]).replace(/\./g, "/"), "YYYY/MM/DD HH:mm:ss");
dataObj = countlyCommon.getDescendantProp(db, activeDateArr[i] + metric);
}
else if (countlyCommon.periodObj.daysInPeriod === 1 && !disableHours) {
formattedDate = moment((activeDate + " " + i + ":00:00").replace(/\./g, "/"), "YYYY/MM/DD HH:mm:ss");
dataObj = countlyCommon.getDescendantProp(db, activeDate + "." + i + metric);
}
else {
formattedDate = moment((activeDateArr[i]).replace(/\./g, "/"), "YYYY/MM/DD");
dataObj = countlyCommon.getDescendantProp(db, activeDateArr[i] + metric);
}
dataObj = clearFunction(dataObj);
if (!tableData[counter]) {
tableData[counter] = {};
}
tableData[counter].date = countlyCommon.formatDate(formattedDate, dateString);
var propertyValue = "";
if (propertyFunctions[j]) {
propertyValue = propertyFunctions[j](dataObj);
}
else {
propertyValue = dataObj[propertyNames[j]];
}
chartData[j].data[chartData[j].data.length] = [counter, propertyValue];
tableData[counter][propertyNames[j]] = propertyValue;
}
}
}
var keyEvents = [];
for (var k = 0; k < chartData.length; k++) {
var flatChartData = _.flatten(chartData[k].data);
var chartVals = _.reject(flatChartData, function(context, value) {
return value % 2 === 0;
});
keyEvents[k] = {};
keyEvents[k].min = _.min(chartVals);
keyEvents[k].max = _.max(chartVals);
}
return { "chartDP": chartData, "chartData": _.compact(tableData), "keyEvents": keyEvents };
};
/**
* Extract two level data with metrics/segments, like total user data from carriers collection
* @memberof countlyCommon
* @param {object} db - countly standard metric data object
* @param {object} rangeArray - array of all metrics/segments to extract (usually what is contained in meta)
* @param {function} clearFunction - function to prefill all expected properties as u, t, n, etc with 0, so you would not have null in the result which won't work when drawing graphs
* @param {object} dataProperties - describing which properties and how to extract
* @param {object=} estOverrideMetric - data from total users api request to correct unique user values
* @returns {object} object to use in bar and pie charts with {"chartData":_.compact(tableData)}
* @example <caption>Extracting carriers data from carriers collection</caption>
* var chartData = countlyCommon.extractTwoLevelData(_carrierDb, ["At&t", "Verizon"], countlyCarrier.clearObject, [
* {
* name:"carrier",
* func:function (rangeArr, dataObj) {
* return rangeArr;
* }
* },
* { "name":"t" },
* { "name":"u" },
* { "name":"n" }
* ]);
* @example <caption>Return data</caption>
* {"chartData":['
* {"carrier":"At&t","t":71,"u":62,"n":36},
* {"carrier":"Verizon","t":66,"u":60,"n":30}
* ]}
*/
countlyCommon.extractTwoLevelData = function(db, rangeArray, clearFunction, dataProperties, estOverrideMetric) {
countlyCommon.periodObj = getPeriodObj();
var periodMin = 0,
periodMax = 0,
dataObj = {},
tableData = [],
propertyNames = _.pluck(dataProperties, "name"),
propertyFunctions = _.pluck(dataProperties, "func"),
propertyValue = 0;
if (!rangeArray) {
return { "chartData": tableData };
}
if (!countlyCommon.periodObj.isSpecialPeriod) {
periodMin = countlyCommon.periodObj.periodMin;
periodMax = (countlyCommon.periodObj.periodMax + 1);
}
else {
periodMin = 0;
periodMax = countlyCommon.periodObj.currentPeriodArr.length;
}
var tableCounter = 0;
var j = 0;
var k = 0;
var i = 0;
if (!countlyCommon.periodObj.isSpecialPeriod) {
for (j = 0; j < rangeArray.length; j++) {
dataObj = countlyCommon.getDescendantProp(db, countlyCommon.periodObj.activePeriod + "." + rangeArray[j]);
if (!dataObj) {
continue;
}
var tmpPropertyObj1 = {};
dataObj = clearFunction(dataObj);
var propertySum = 0;
for (k = 0; k < propertyNames.length; k++) {
if (propertyFunctions[k]) {
propertyValue = propertyFunctions[k](rangeArray[j], dataObj);
}
else {
propertyValue = dataObj[propertyNames[k]];
}
if (typeof propertyValue !== 'string') {
propertySum += propertyValue;
}
tmpPropertyObj1[propertyNames[k]] = propertyValue;
}
if (propertySum > 0) {
tableData[tableCounter] = {};
tableData[tableCounter] = tmpPropertyObj1;
tableCounter++;
}
}
}
else {
var calculatedObj = (estOverrideMetric) ? countlyTotalUsers.get(estOverrideMetric) : {};
for (j = 0; j < rangeArray.length; j++) {
var tmp_x = {};
var tmpPropertyObj = {};
for (i = periodMin; i < periodMax; i++) {
dataObj = countlyCommon.getDescendantProp(db, countlyCommon.periodObj.currentPeriodArr[i] + "." + rangeArray[j]);
if (!dataObj) {
continue;
}
dataObj = clearFunction(dataObj);
for (k = 0; k < propertyNames.length; k++) {
if (propertyNames[k] === "u") {
propertyValue = 0;
}
else if (propertyFunctions[k]) {
propertyValue = propertyFunctions[k](rangeArray[j], dataObj);
}
else {
propertyValue = dataObj[propertyNames[k]];
}
if (!tmpPropertyObj[propertyNames[k]]) {
tmpPropertyObj[propertyNames[k]] = 0;
}
if (typeof propertyValue === 'string') {
tmpPropertyObj[propertyNames[k]] = propertyValue;
}
else {
tmpPropertyObj[propertyNames[k]] += propertyValue;
}
}
}
if (propertyNames.indexOf("u") !== -1 && Object.keys(tmpPropertyObj).length) {
if (countlyTotalUsers.isUsable() && estOverrideMetric && typeof calculatedObj[rangeArray[j]] !== "undefined") {
tmpPropertyObj.u = calculatedObj[rangeArray[j]];
}
else {
var tmpUniqVal = 0,
tmpUniqValCheck = 0,
tmpCheckVal = 0,
l = 0;
for (l = 0; l < (countlyCommon.periodObj.uniquePeriodArr.length); l++) {
tmp_x = countlyCommon.getDescendantProp(db, countlyCommon.periodObj.uniquePeriodArr[l] + "." + rangeArray[j]);
if (!tmp_x) {
continue;
}
tmp_x = clearFunction(tmp_x);
propertyValue = tmp_x.u;
if (typeof propertyValue === 'string') {
tmpPropertyObj.u = propertyValue;
}
else {
tmpUniqVal += propertyValue;
tmpPropertyObj.u += propertyValue;
}
}
for (l = 0; l < (countlyCommon.periodObj.uniquePeriodCheckArr.length); l++) {
tmp_x = countlyCommon.getDescendantProp(db, countlyCommon.periodObj.uniquePeriodCheckArr[l] + "." + rangeArray[j]);
if (!tmp_x) {
continue;
}
tmp_x = clearFunction(tmp_x);
tmpCheckVal = tmp_x.u;
if (typeof tmpCheckVal !== 'string') {
tmpUniqValCheck += tmpCheckVal;
}
}
if (tmpUniqVal > tmpUniqValCheck) {
tmpPropertyObj.u = tmpUniqValCheck;
}
}
// Total users can't be less than new users
if (tmpPropertyObj.u < tmpPropertyObj.n) {
if (countlyTotalUsers.isUsable() && estOverrideMetric && typeof calculatedObj[rangeArray[j]] !== "undefined") {
tmpPropertyObj.n = calculatedObj[rangeArray[j]];
}
else {
tmpPropertyObj.u = tmpPropertyObj.n;
}
}
// Total users can't be more than total sessions
if (tmpPropertyObj.u > tmpPropertyObj.t) {
tmpPropertyObj.u = tmpPropertyObj.t;
}
}
tableData[tableCounter] = {};
tableData[tableCounter] = tmpPropertyObj;
tableCounter++;
}
}
for (i = 0; i < tableData.length; i++) {
if (_.isEmpty(tableData[i])) {
tableData[i] = null;
}
}
tableData = _.compact(tableData);
if (propertyNames.indexOf("u") !== -1) {
countlyCommon.sortByProperty(tableData, "u");
}
else if (propertyNames.indexOf("t") !== -1) {
countlyCommon.sortByProperty(tableData, "t");
}
else if (propertyNames.indexOf("c") !== -1) {
countlyCommon.sortByProperty(tableData, "c");
}
return { "chartData": tableData };
};
countlyCommon.sortByProperty = function(tableData, prop) {
tableData.sort(function(a, b) {
a = (a && a[prop]) ? a[prop] : 0;
b = (b && b[prop]) ? b[prop] : 0;
return b - a;
});
};
/**
* Merge metric data in chartData returned by @{link countlyCommon.extractChartData} or @{link countlyCommon.extractTwoLevelData }, just in case if after data transformation of countly standard metric data model, resulting chartData contains duplicated values, as for example converting null, undefined and unknown values to unknown
* @memberof countlyCommon
* @param {object} chartData - chartData returned by @{link countlyCommon.extractChartData} or @{link countlyCommon.extractTwoLevelData }
* @param {string} metric - metric name to merge
* @returns {object} chartData object with same metrics summed up
* @example <caption>Sample input</caption>
* {"chartData":[
* {"metric":"Test","t":71,"u":62,"n":36},
* {"metric":"Test1","t":66,"u":60,"n":30},
* {"metric":"Test","t":2,"u":3,"n":4}
* ]}
* @example <caption>Sample output</caption>
* {"chartData":[
* {"metric":"Test","t":73,"u":65,"n":40},
* {"metric":"Test1","t":66,"u":60,"n":30}
* ]}
*/
countlyCommon.mergeMetricsByName = function(chartData, metric) {
var uniqueNames = {},
data;
for (var i = 0; i < chartData.length; i++) {
data = chartData[i];
var newName = (data[metric] + "").trim();
if (newName === "") {
newName = jQuery.i18n.map["common.unknown"];
}
data[metric] = newName;
if (newName && !uniqueNames[newName]) {
uniqueNames[newName] = data;
}
else {
for (var key in data) {
if (typeof data[key] === "string") {
uniqueNames[newName][key] = data[key];
}
else if (typeof data[key] === "number") {
if (!uniqueNames[newName][key]) {
uniqueNames[newName][key] = 0;
}
uniqueNames[newName][key] += data[key];
}
}
}
}
return _.values(uniqueNames);
};
/**
* Extracts top three items (from rangeArray) that have the biggest total session counts from the db object.
* @memberof countlyCommon
* @param {object} db - countly standard metric data object
* @param {object} rangeArray - array of all metrics/segments to extract (usually what is contained in meta)
* @param {function} clearFunction - function to prefill all expected properties as u, t, n, etc with 0, so you would not have null in the result which won't work when drawing graphs
* @param {function} fetchFunction - function to fetch property, default used is function (rangeArr, dataObj) {return rangeArr;}
* @param {String} metric - name of the metric to use ordering and returning
* @param {string} estOverrideMetric - name of the total users estimation override, by default will use default _estOverrideMetric provided on initialization
* @param {function} fixBarSegmentData - function to make any adjustments to the extracted data based on segment
* @returns {array} array with top 3 values
* @example <caption>Return data</caption>
* [
* {"name":"iOS","percent":35},
* {"name":"Android","percent":33},
* {"name":"Windows Phone","percent":32}
* ]
*/
countlyCommon.extractBarDataWPercentageOfTotal = function(db, rangeArray, clearFunction, fetchFunction, metric, estOverrideMetric, fixBarSegmentData) {
fetchFunction = fetchFunction || function(rangeArr) {
return rangeArr;
};
var rangeData = countlyCommon.extractTwoLevelData(db, rangeArray, clearFunction, [
{
name: "range",
func: fetchFunction
},
{ "name": metric }
], estOverrideMetric);
return countlyCommon.calculateBarDataWPercentageOfTotal(rangeData, metric, fixBarSegmentData);
};
/**
* Extracts top three items (from rangeArray) that have the biggest total session counts from the db object.
* @memberof countlyCommon
* @param {object} db - countly standard metric data object
* @param {object} rangeArray - array of all metrics/segments to extract (usually what is contained in meta)
* @param {function} clearFunction - function to prefill all expected properties as u, t, n, etc with 0, so you would not have null in the result which won't work when drawing graphs
* @param {function} fetchFunction - function to fetch property, default used is function (rangeArr, dataObj) {return rangeArr;}
* @returns {array} array with top 3 values
* @example <caption>Return data</caption>
* [
* {"name":"iOS","percent":35},
* {"name":"Android","percent":33},
* {"name":"Windows Phone","percent":32}
* ]
*/
countlyCommon.extractBarData = function(db, rangeArray, clearFunction, fetchFunction) {
fetchFunction = fetchFunction || function(rangeArr) {
return rangeArr;
};
var rangeData = countlyCommon.extractTwoLevelData(db, rangeArray, clearFunction, [
{
name: "range",
func: fetchFunction
},
{ "name": "t" }
]);
return countlyCommon.calculateBarData(rangeData);
};
/**
* Extracts top three items (from rangeArray) that have the biggest total session counts from the chartData with their percentage of total
* @memberof countlyCommon
* @param {object} rangeData - chartData retrieved from {@link countlyCommon.extractTwoLevelData} as {"chartData":[{"carrier":"At&t","t":71,"u":62,"n":36},{"carrier":"Verizon","t":66,"u":60,"n":30}]}
* @param {String} metric - name of the metric to use ordering and returning
* @param {Function} fixBarSegmentData - Function to fix bar data segment data
* @returns {array} array with top 3 values
* @example <caption>Return data</caption>
* [
* {"name":"iOS","percent":44},
* {"name":"Android","percent":22},
* {"name":"Windows Phone","percent":14}
* ]
*/
countlyCommon.calculateBarDataWPercentageOfTotal = function(rangeData, metric, fixBarSegmentData) {
rangeData.chartData = countlyCommon.mergeMetricsByName(rangeData.chartData, "range");
if (fixBarSegmentData) {
rangeData = fixBarSegmentData(rangeData);
}
rangeData.chartData = _.sortBy(rangeData.chartData, function(obj) {
return -obj[metric];
});
var rangeNames = _.pluck(rangeData.chartData, 'range'),
rangeTotal = _.pluck(rangeData.chartData, metric),
barData = [],
maxItems = 3,
totalSum = 0;
rangeTotal.forEach(function(r) {
totalSum += r;
});
rangeTotal.sort(function(a, b) {
if (a < b) {
return 1;
}
if (b < a) {
return -1;
}
return 0;
});
var totalPercent = 0;
for (var i = rangeNames.length - 1; i >= 0; i--) {
var percent = countlyCommon.round((rangeTotal[i] / totalSum) * 100, 1);
totalPercent += percent;
barData[i] = { "name": rangeNames[i], "percent": percent };
}
var deltaFixEl = 0;
if (totalPercent < 100) {
//Add the missing delta to the first value
deltaFixEl = 0;
}
else if (totalPercent > 100) {
//Subtract the extra delta from the last value
deltaFixEl = barData.length - 1;
}
if (barData.length > 0) {
barData[deltaFixEl].percent += 100 - totalPercent;
barData[deltaFixEl].percent = countlyCommon.round(barData[deltaFixEl].percent, 1);
}
if (rangeNames.length < maxItems) {
maxItems = rangeNames.length;
}
return barData.slice(0, maxItems);
};
/**
* Extracts top three items (from rangeArray) that have the biggest total session counts from the chartData.
* @memberof countlyCommon
* @param {object} rangeData - chartData retrieved from {@link countlyCommon.extractTwoLevelData} as {"chartData":[{"carrier":"At&t","t":71,"u":62,"n":36},{"carrier":"Verizon","t":66,"u":60,"n":30}]}
* @returns {array} array with top 3 values
* @example <caption>Return data</caption>
* [
* {"name":"iOS","percent":35},
* {"name":"Android","percent":33},
* {"name":"Windows Phone","percent":32}
* ]
*/
countlyCommon.calculateBarData = function(rangeData) {
rangeData.chartData = countlyCommon.mergeMetricsByName(rangeData.chartData, "range");
rangeData.chartData = _.sortBy(rangeData.chartData, function(obj) {
return -obj.t;
});
var rangeNames = _.pluck(rangeData.chartData, 'range'),
rangeTotal = _.pluck(rangeData.chartData, 't'),
barData = [],
sum = 0,
maxItems = 3,
totalPercent = 0;
rangeTotal.sort(function(a, b) {
if (a < b) {
return 1;
}
if (b < a) {
return -1;
}
return 0;
});
if (rangeNames.length < maxItems) {
maxItems = rangeNames.length;
}
var i = 0;
for (i = 0; i < maxItems; i++) {
sum += rangeTotal[i];
}
for (i = maxItems - 1; i >= 0; i--) {
var percent = Math.floor((rangeTotal[i] / sum) * 100);
totalPercent += percent;
if (i === 0) {
percent += 100 - totalPercent;
}
barData[i] = { "name": rangeNames[i], "percent": percent };
}
return barData;
};
countlyCommon.extractUserChartData = function(db, label, sec) {
var ret = { "data": [], "label": label };
countlyCommon.periodObj = getPeriodObj();
var periodMin, periodMax, dateob;
if (countlyCommon.periodObj.isSpecialPeriod) {
periodMin = 0;
periodMax = (countlyCommon.periodObj.daysInPeriod);
var dateob1 = countlyCommon.processPeriod(countlyCommon.periodObj.currentPeriodArr[0].toString());
var dateob2 = countlyCommon.processPeriod(countlyCommon.periodObj.currentPeriodArr[countlyCommon.periodObj.currentPeriodArr.length - 1].toString());
dateob = { timestart: dateob1.timestart, timeend: dateob2.timeend, range: "d" };
}
else {
periodMin = countlyCommon.periodObj.periodMin;
periodMax = countlyCommon.periodObj.periodMax + 1;
dateob = countlyCommon.processPeriod(countlyCommon.periodObj.activePeriod.toString());
}
var res = [],
ts;
//get all timestamps in that period
var i = 0;
for (i = 0, l = db.length; i < l; i++) {
ts = db[i];
if (sec) {
ts.ts = ts.ts * 1000;
}
if (ts.ts > dateob.timestart && ts.ts <= dateob.timeend) {
res.push(ts);
}
}
var lastStart,
lastEnd = dateob.timestart,
total,
data = ret.data;
for (i = periodMin; i < periodMax; i++) {
total = 0;
lastStart = lastEnd;
lastEnd = moment(lastStart).add(moment.duration(1, dateob.range)).valueOf();
for (var j = 0, l = res.length; j < l; j++) {
ts = res[j];
if (ts.ts > lastStart && ts.ts <= lastEnd) {
if (ts.c) {
total += ts.c;
}
else {
total++;
}
}
}
data.push([i, total]);
}
return ret;
};
countlyCommon.processPeriod = function(period) {
var date = period.split(".");
var range,
timestart,
timeend;
if (date.length === 1) {
range = "M";
timestart = moment(period, "YYYY").valueOf();
timeend = moment(period, "YYYY").add(moment.duration(1, "y")).valueOf();
}
else if (date.length === 2) {
range = "d";
timestart = moment(period, "YYYY.MM").valueOf();
timeend = moment(period, "YYYY.MM").add(moment.duration(1, "M")).valueOf();
}
else if (date.length === 3) {
range = "h";
timestart = moment(period, "YYYY.MM.DD").valueOf();
timeend = moment(period, "YYYY.MM.DD").add(moment.duration(1, "d")).valueOf();
}
return { timestart: timestart, timeend: timeend, range: range };
};
/**
* Shortens the given number by adding K (thousand) or M (million) postfix. K is added only if the number is bigger than 10000, etc.
* @memberof countlyCommon
* @param {number} number - number to shorten
* @returns {string} shorter representation of number
* @example
* //outputs 10K
* countlyCommon.getShortNumber(10000);
*/
countlyCommon.getShortNumber = function(number) {
var tmpNumber = "";
if (number >= 1000000000 || number <= -1000000000) {
tmpNumber = ((number / 1000000000).toFixed(1).replace(".0", "")) + "B";
}
else if (number >= 1000000 || number <= -1000000) {
tmpNumber = ((number / 1000000).toFixed(1).replace(".0", "")) + "M";
}
else if (number >= 10000 || number <= -10000) {
tmpNumber = ((number / 1000).toFixed(1).replace(".0", "")) + "K";
}
else if (number >= 0.1 || number <= -0.1) {
number += "";
tmpNumber = number.replace(".0", "");
}
else {
tmpNumber = number + "";
}
return tmpNumber;
};
/**
* Getting the date range shown on the dashboard like 1 Aug - 30 Aug, using {@link countlyCommon.periodObj) dateString property which holds the date format.
* @memberof countlyCommon
* @returns {string} string with formatted date range as 1 Aug - 30 Aug
*/
countlyCommon.getDateRange = function() {
countlyCommon.periodObj = getPeriodObj();
var formattedDateStart = "";
var formattedDateEnd = "";
if (!countlyCommon.periodObj.isSpecialPeriod) {
if (countlyCommon.periodObj.dateString === "HH:mm") {
formattedDateStart = moment(countlyCommon.periodObj.activePeriod + " " + countlyCommon.periodObj.periodMin + ":00", "YYYY.M.D HH:mm");
formattedDateEnd = moment(countlyCommon.periodObj.activePeriod + " " + countlyCommon.periodObj.periodMax + ":00", "YYYY.M.D HH:mm");
var nowMin = moment().format("mm");
formattedDateEnd.add(nowMin, "minutes");
}
else if (countlyCommon.periodObj.dateString === "D MMM, HH:mm") {
formattedDateStart = moment(countlyCommon.periodObj.activePeriod, "YYYY.M.D");
formattedDateEnd = moment(countlyCommon.periodObj.activePeriod, "YYYY.M.D").add(23, "hours").add(59, "minutes");
}
else {
formattedDateStart = moment(countlyCommon.periodObj.activePeriod + "." + countlyCommon.periodObj.periodMin, "YYYY.M.D");
formattedDateEnd = moment(countlyCommon.periodObj.activePeriod + "." + countlyCommon.periodObj.periodMax, "YYYY.M.D");
}
}
else {
formattedDateStart = moment(countlyCommon.periodObj.currentPeriodArr[0], "YYYY.M.D");
formattedDateEnd = moment(countlyCommon.periodObj.currentPeriodArr[(countlyCommon.periodObj.currentPeriodArr.length - 1)], "YYYY.M.D");
}
var fromStr = countlyCommon.formatDate(formattedDateStart, countlyCommon.periodObj.dateString),
toStr = countlyCommon.formatDate(formattedDateEnd, countlyCommon.periodObj.dateString);
if (fromStr === toStr) {
return fromStr;
}
else {
return fromStr + " - " + toStr;
}
};
countlyCommon.getDateRangeForCalendar = function() {
countlyCommon.periodObj = getPeriodObj();
var formattedDateStart = "";
var formattedDateEnd = "";
if (!countlyCommon.periodObj.isSpecialPeriod) {
if (countlyCommon.periodObj.dateString === "HH:mm") {
formattedDateStart = countlyCommon.formatDate(moment(countlyCommon.periodObj.activePeriod + " " + countlyCommon.periodObj.periodMin + ":00", "YYYY.M.D HH:mm"), "D MMM, YYYY HH:mm");
formattedDateEnd = moment(countlyCommon.periodObj.activePeriod + " " + countlyCommon.periodObj.periodMax + ":00", "YYYY.M.D HH:mm");
formattedDateEnd = formattedDateEnd.add(59, "minutes");
formattedDateEnd = countlyCommon.formatDate(formattedDateEnd, "D MMM, YYYY HH:mm");
}
else if (countlyCommon.periodObj.dateString === "D MMM, HH:mm") {
formattedDateStart = countlyCommon.formatDate(moment(countlyCommon.periodObj.activePeriod, "YYYY.M.D"), "D MMM, YYYY HH:mm");
formattedDateEnd = countlyCommon.formatDate(moment(countlyCommon.periodObj.activePeriod, "YYYY.M.D").add(23, "hours").add(59, "minutes"), "D MMM, YYYY HH:mm");
}
else if (countlyCommon.periodObj.dateString === "MMM") { //this year
formattedDateStart = countlyCommon.formatDate(moment(countlyCommon.periodObj.activePeriod + "." + countlyCommon.periodObj.periodMin + ".1", "YYYY.M.D"), "D MMM, YYYY");
formattedDateEnd = countlyCommon.formatDate(moment(countlyCommon.periodObj.activePeriod + "." + countlyCommon.periodObj.periodMax + ".31", "YYYY.M.D"), "D MMM, YYYY");
}
else {
formattedDateStart = countlyCommon.formatDate(moment(countlyCommon.periodObj.activePeriod + "." + countlyCommon.periodObj.periodMin, "YYYY.M.D"), "D MMM, YYYY");
formattedDateEnd = countlyCommon.formatDate(moment(countlyCommon.periodObj.activePeriod + "." + countlyCommon.periodObj.periodMax, "YYYY.M.D"), "D MMM, YYYY");
}
}
else {
formattedDateStart = countlyCommon.formatDate(moment(countlyCommon.periodObj.currentPeriodArr[0], "YYYY.M.D"), "D MMM, YYYY");
formattedDateEnd = countlyCommon.formatDate(moment(countlyCommon.periodObj.currentPeriodArr[(countlyCommon.periodObj.currentPeriodArr.length - 1)], "YYYY.M.D"), "D MMM, YYYY");
}
return formattedDateStart + " - " + formattedDateEnd;
};
/**
* Merge standard countly metric data object, by mergin updateObj retrieved from action=refresh api requests object into dbObj.
* Used for merging the received data for today to the existing data while updating the dashboard.
* @memberof countlyCommon
* @param {object} dbObj - standard metric data object
* @param {object} updateObj - standard metric data object retrieved from action=refresh request to last time bucket data only
*/
countlyCommon.extendDbObj = function(dbObj, updateObj) {
var now = moment(),
year = now.year(),
month = (now.month() + 1),
day = now.date(),
weekly = Math.ceil(now.format("DDD") / 7),
intRegex = /^\d+$/,
tmpUpdateObj = {},
tmpOldObj = {};
if (updateObj[year] && updateObj[year][month] && updateObj[year][month][day]) {
if (!dbObj[year]) {
dbObj[year] = {};
}
if (!dbObj[year][month]) {
dbObj[year][month] = {};
}
if (!dbObj[year][month][day]) {
dbObj[year][month][day] = {};
}
if (!dbObj[year]["w" + weekly]) {
dbObj[year]["w" + weekly] = {};
}
tmpUpdateObj = updateObj[year][month][day];
tmpOldObj = dbObj[year][month][day];
dbObj[year][month][day] = updateObj[year][month][day];
}
if (updateObj.meta) {
if (!dbObj.meta) {
dbObj.meta = {};
}
dbObj.meta = updateObj.meta;
}
for (var level1 in tmpUpdateObj) {
if (!Object.prototype.hasOwnProperty.call(tmpUpdateObj, level1)) {
continue;
}
if (intRegex.test(level1)) {
continue;
}
if (_.isObject(tmpUpdateObj[level1])) {
if (!dbObj[year][level1]) {
dbObj[year][level1] = {};
}
if (!dbObj[year][month][level1]) {
dbObj[year][month][level1] = {};
}
if (!dbObj[year]["w" + weekly][level1]) {
dbObj[year]["w" + weekly][level1] = {};
}
}
else {
if (dbObj[year][level1]) {
if (tmpOldObj[level1]) {
dbObj[year][level1] += (tmpUpdateObj[level1] - tmpOldObj[level1]);
}
else {
dbObj[year][level1] += tmpUpdateObj[level1];
}
}
else {
dbObj[year][level1] = tmpUpdateObj[level1];
}
if (dbObj[year][month][level1]) {
if (tmpOldObj[level1]) {
dbObj[year][month][level1] += (tmpUpdateObj[level1] - tmpOldObj[level1]);
}
else {
dbObj[year][month][level1] += tmpUpdateObj[level1];
}
}
else {
dbObj[year][month][level1] = tmpUpdateObj[level1];
}
if (dbObj[year]["w" + weekly][level1]) {
if (tmpOldObj[level1]) {
dbObj[year]["w" + weekly][level1] += (tmpUpdateObj[level1] - tmpOldObj[level1]);
}
else {
dbObj[year]["w" + weekly][level1] += tmpUpdateObj[level1];
}
}
else {
dbObj[year]["w" + weekly][level1] = tmpUpdateObj[level1];
}
}
if (tmpUpdateObj[level1]) {
for (var level2 in tmpUpdateObj[level1]) {
if (!Object.prototype.hasOwnProperty.call(tmpUpdateObj[level1], level2)) {
continue;
}
if (dbObj[year][level1][level2]) {
if (tmpOldObj[level1] && tmpOldObj[level1][level2]) {
dbObj[year][level1][level2] += (tmpUpdateObj[level1][level2] - tmpOldObj[level1][level2]);
}
else {
dbObj[year][level1][level2] += tmpUpdateObj[level1][level2];
}
}
else {
dbObj[year][level1][level2] = tmpUpdateObj[level1][level2];
}
if (dbObj[year][month][level1][level2]) {
if (tmpOldObj[level1] && tmpOldObj[level1][level2]) {
dbObj[year][month][level1][level2] += (tmpUpdateObj[level1][level2] - tmpOldObj[level1][level2]);
}
else {
dbObj[year][month][level1][level2] += tmpUpdateObj[level1][level2];
}
}
else {
dbObj[year][month][level1][level2] = tmpUpdateObj[level1][level2];
}
if (dbObj[year]["w" + weekly][level1][level2]) {
if (tmpOldObj[level1] && tmpOldObj[level1][level2]) {
dbObj[year]["w" + weekly][level1][level2] += (tmpUpdateObj[level1][level2] - tmpOldObj[level1][level2]);
}
else {
dbObj[year]["w" + weekly][level1][level2] += tmpUpdateObj[level1][level2];
}
}
else {
dbObj[year]["w" + weekly][level1][level2] = tmpUpdateObj[level1][level2];
}
}
}
}
// Fix update of total user count
if (updateObj[year]) {
if (updateObj[year].u) {
if (!dbObj[year]) {
dbObj[year] = {};
}
dbObj[year].u = updateObj[year].u;
}
if (updateObj[year][month] && updateObj[year][month].u) {
if (!dbObj[year]) {
dbObj[year] = {};
}
if (!dbObj[year][month]) {
dbObj[year][month] = {};
}
dbObj[year][month].u = updateObj[year][month].u;
}
if (updateObj[year]["w" + weekly] && updateObj[year]["w" + weekly].u) {
if (!dbObj[year]) {
dbObj[year] = {};
}
if (!dbObj[year]["w" + weekly]) {
dbObj[year]["w" + weekly] = {};
}
dbObj[year]["w" + weekly].u = updateObj[year]["w" + weekly].u;
}
}
};
/**
* Convert string to first letter uppercase and all other letters - lowercase for each word
* @memberof countlyCommon
* @param {string} str - string to convert
* @returns {string} converted string
* @example
* //outputs Hello World
* countlyCommon.toFirstUpper("hello world");
*/
countlyCommon.toFirstUpper = function(str) {
return str.replace(/\w\S*/g, function(txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
};
/**
* Safe division between numbers providing 0 as result in cases when dividing by 0
* @memberof countlyCommon
* @param {number} val1 - number which to divide
* @param {number} val2 - number by which to divide
* @returns {number} result of division
* @example
* //outputs 0
* countlyCommon.divide(100, 0);
*/
countlyCommon.divide = function(val1, val2) {
var temp = val1 / val2;
if (!temp || temp === Number.POSITIVE_INFINITY) {
temp = 0;
}
return temp;
};
/**
* Get Date graph ticks
* @memberof countlyCommon
* @param {string} bucket - time bucket, accepted values, hourly, weekly, monthly
* @param {boolean} overrideBucket - override existing bucket logic and simply use current date for generating ticks
* @param {boolean} newChart - new chart implementation
* @returns {object} object containing tick texts and ticks to use on time graphs
* @example <caption>Example output</caption>
*{
* "min":0,
* "max":29,
* "tickTexts":["22 Dec, Thursday","23 Dec, Friday","24 Dec, Saturday","25 Dec, Sunday","26 Dec, Monday","27 Dec, Tuesday","28 Dec, Wednesday",
* "29 Dec, Thursday","30 Dec, Friday","31 Dec, Saturday","1 Jan, Sunday","2 Jan, Monday","3 Jan, Tuesday","4 Jan, Wednesday","5 Jan, Thursday",
* "6 Jan, Friday","7 Jan, Saturday","8 Jan, Sunday","9 Jan, Monday","10 Jan, Tuesday","11 Jan, Wednesday","12 Jan, Thursday","13 Jan, Friday",
* "14 Jan, Saturday","15 Jan, Sunday","16 Jan, Monday","17 Jan, Tuesday","18 Jan, Wednesday","19 Jan, Thursday","20 Jan, Friday"],
* "ticks":[[1,"23 Dec"],[4,"26 Dec"],[7,"29 Dec"],[10,"1 Jan"],[13,"4 Jan"],[16,"7 Jan"],[19,"10 Jan"],[22,"13 Jan"],[25,"16 Jan"],[28,"19 Jan"]]
*}
*/
countlyCommon.getTickObj = function(bucket, overrideBucket, newChart) {
var days = parseInt(countlyCommon.periodObj.numberOfDays, 10),
ticks = [],
tickTexts = [],
skipReduction = false,
limitAdjustment = 0;
if (overrideBucket) {
var thisDay;
if (countlyCommon.periodObj.activePeriod) {
thisDay = moment(countlyCommon.periodObj.activePeriod, "YYYY.M.D");
}
else {
thisDay = moment(countlyCommon.periodObj.currentPeriodArr[0], "YYYY.M.D");
}
ticks.push([0, countlyCommon.formatDate(thisDay, "D MMM")]);
tickTexts[0] = countlyCommon.formatDate(thisDay, "D MMM, dddd");
}
else if ((days === 1 && _period !== "month" && _period !== "day") || (days === 1 && bucket === "hourly")) {
//When period is an array or string like Xdays, Xweeks
for (var z = 0; z < 24; z++) {
ticks.push([z, (z + ":00")]);
tickTexts.push((z + ":00"));
}
skipReduction = true;
}
else {
var start = moment().subtract(days, 'days');
if (Object.prototype.toString.call(countlyCommon.getPeriod()) === '[object Array]') {
start = moment(countlyCommon.periodObj.currentPeriodArr[countlyCommon.periodObj.currentPeriodArr.length - 1], "YYYY.MM.DD").subtract(days, 'days');
}
var i = 0;
if (bucket === "monthly") {
var allMonths = [];
//so we would not start from previous year
start.add(1, 'day');
var monthCount = 12;
for (i = 0; i < monthCount; i++) {
allMonths.push(start.format(countlyCommon.getDateFormat("MMM YYYY")));
start.add(1, 'months');
}
allMonths = _.uniq(allMonths);
for (i = 0; i < allMonths.length; i++) {
ticks.push([i, allMonths[i]]);
tickTexts[i] = allMonths[i];
}
}
else if (bucket === "weekly") {
var allWeeks = [];
for (i = 0; i < days; i++) {
start.add(1, 'days');
if (i === 0 && start.isoWeekday() === 7) {
continue;
}
allWeeks.push(start.isoWeek() + " " + start.isoWeekYear());
}
allWeeks = _.uniq(allWeeks);
for (i = 0; i < allWeeks.length; i++) {
var parts = allWeeks[i].split(" ");
//iso week falls in the year which has thursday of the week
if (parseInt(parts[1]) === moment().isoWeekYear(parseInt(parts[1])).isoWeek(parseInt(parts[0])).isoWeekday(4).year()) {
ticks.push([i, "W" + allWeeks[i]]);
var weekText = countlyCommon.formatDate(moment().isoWeekYear(parseInt(parts[1])).isoWeek(parseInt(parts[0])).isoWeekday(1), ", D MMM YYYY");
tickTexts[i] = "W" + parts[0] + weekText;
}
}
}
else if (bucket === "hourly") {
for (i = 0; i < days; i++) {
start.add(1, 'days');
for (var j = 0; j < 24; j++) {
//if (j === 0) {
ticks.push([((24 * i) + j), countlyCommon.formatDate(start, "D MMM") + " 0:00"]);
//}
tickTexts.push(countlyCommon.formatDate(start, "D MMM, ") + j + ":00");
}
}
}
else {
if (_period === "day") {
start.add(1, 'days');
var now = new Date();
// it will add the count of days of the current month to the x-axis label
var currentMonthCount = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
for (i = 0; i < currentMonthCount; i++) {
ticks.push([i, countlyCommon.formatDate(start, "D MMM")]);
tickTexts[i] = countlyCommon.formatDate(start, "D MMM, dddd");
start.add(1, 'days');
}
}
else if (_period === "prevMonth") {
start = moment().subtract(1, "month").startOf("month");
//start.add(1,"days");
let current = new Date();
let prevMonthCount = new Date(current.getFullYear(), current.getMonth(), 0).getDate();
for (i = 0; i < prevMonthCount; i++) {
ticks.push([i, countlyCommon.formatDate(start, "D MMM")]);
tickTexts[i] = countlyCommon.formatDate(start, "D MMM, dddd");
start.add(1, 'days');
}
}
else {
var startYear = start.year();
var endYear = moment().year();
for (i = 0; i < days; i++) {
start.add(1, 'days');
if (startYear < endYear) {
ticks.push([i, countlyCommon.formatDate(start, "D MMM YYYY")]);
tickTexts[i] = countlyCommon.formatDate(start, "D MMM YYYY, dddd");
}
else {
ticks.push([i, countlyCommon.formatDate(start, "D MMM")]);
tickTexts[i] = countlyCommon.formatDate(start, "D MMM, dddd");
}
}
}
}
ticks = _.compact(ticks);
tickTexts = _.compact(tickTexts);
}
var labelCn = ticks.length;
if (!newChart) {
if (ticks.length <= 2) {
limitAdjustment = 0.02;
var tmpTicks = [],
tmpTickTexts = [];
tmpTickTexts[0] = "";
tmpTicks[0] = [-0.02, ""];
for (var m = 0; m < ticks.length; m++) {
tmpTicks[m + 1] = [m, ticks[m][1]];
tmpTickTexts[m + 1] = tickTexts[m];
}
tmpTickTexts.push("");
tmpTicks.push([tmpTicks.length - 1 - 0.98, ""]);
ticks = tmpTicks;
tickTexts = tmpTickTexts;
}
else if (!skipReduction && ticks.length > 10) {
var reducedTicks = [],
step = (Math.floor(ticks.length / 10) < 1) ? 1 : Math.floor(ticks.length / 10),
pickStartIndex = (Math.floor(ticks.length / 30) < 1) ? 1 : Math.floor(ticks.length / 30);
for (var l = pickStartIndex; l < (ticks.length - 1); l = l + step) {
reducedTicks.push(ticks[l]);
}
ticks = reducedTicks;
}
else {
ticks[0] = null;
// Hourly ticks already contain 23 empty slots at the end
if (!(bucket === "hourly" && days !== 1)) {
ticks[ticks.length - 1] = null;
}
}
}
return {
min: 0 - limitAdjustment,
max: (limitAdjustment) ? tickTexts.length - 3 + limitAdjustment : tickTexts.length - 1,
tickTexts: tickTexts,
ticks: _.compact(ticks),
labelCn: labelCn
};
};
/**
* Joined 2 arrays into one removing all duplicated values
* @memberof countlyCommon
* @param {array} x - first array
* @param {array} y - second array
* @returns {array} new array with only unique values from x and y
* @example
* //outputs [1,2,3]
* countlyCommon.union([1,2],[2,3]);
*/
countlyCommon.union = function(x, y) {
if (!x) {
return y;
}
else if (!y) {
return x;
}
var obj = {};
var i = 0;
for (i = x.length - 1; i >= 0; --i) {
obj[x[i]] = true;
}
for (i = y.length - 1; i >= 0; --i) {
obj[y[i]] = true;
}
var res = [];
for (var k in obj) {
res.push(k);
}
return res;
};
/**
* Recursively merges an object into another
* @memberof countlyCommon
* @param {Object} target - object to be merged into
* @param {Object} source - object to merge into the target
* @returns {Object} target after the merge
*/
countlyCommon.deepObjectExtend = function(target, source) {
return mergeWith({}, target, source);
};
/**
* Formats the number by separating each 3 digits with
* @memberof countlyCommon
* @param {number} x - number to format
* @returns {string} formatted number
* @example
* //outputs 1,234,567
* countlyCommon.formatNumber(1234567);
*/
countlyCommon.formatNumber = function(x) {
x = parseFloat(parseFloat(x).toFixed(2));
var parts = x.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return parts.join(".");
};
/**
* Formats the number by separating each 3 digits with, falls back to
* a default value in case of NaN
* @memberof countlyCommon
* @param {number} x - number to format
* @param {string} fallback - fallback value for unparsable numbers
* @returns {string} formatted number or fallback
* @example
* //outputs 1,234,567
* countlyCommon.formatNumberSafe(1234567);
*/
countlyCommon.formatNumberSafe = function(x, fallback) {
if (isNaN(parseFloat(x))) {
return fallback || "N/A";
}
return countlyCommon.formatNumber(x);
};
/**
* Pad number with specified character from left to specified length
* @memberof countlyCommon
* @param {number} n - number to pad
* @param {number} width - pad to what length in symboles
* @param {string} z - character to pad with, default 0
* @returns {string} padded number
* @example
* //outputs 0012
* countlyCommon.pad(12, 4, "0");
*/
countlyCommon.pad = function(n, width, z) {
z = z || '0';
n = n + '';
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
};
countlyCommon.getNoteDateIds = function(bucket) {
var _periodObj = countlyCommon.periodObj,
dateIds = [],
dotSplit = [],
tmpDateStr = "";
var i = 0;
var j = 0;
if (!_periodObj.isSpecialPeriod && !bucket) {
for (i = _periodObj.periodMin; i < (_periodObj.periodMax + 1); i++) {
dotSplit = (_periodObj.activePeriod + "." + i).split(".");
tmpDateStr = "";
for (j = 0; j < dotSplit.length; j++) {
if (dotSplit[j].length === 1) {
tmpDateStr += "0" + dotSplit[j];
}
else {
tmpDateStr += dotSplit[j];
}
}
dateIds.push(tmpDateStr);
}
}
else {
if (!_periodObj.currentPeriodArr && bucket === "daily") {
var tmpDate = new Date();
_periodObj.currentPeriodArr = [];
if (countlyCommon.getPeriod() === "month") {
for (i = 0; i < (tmpDate.getMonth() + 1); i++) {
var daysInMonth = moment().month(i).daysInMonth();
for (j = 0; j < daysInMonth; j++) {
_periodObj.currentPeriodArr.push(_periodObj.activePeriod + "." + (i + 1) + "." + (j + 1));
// If current day of current month, just break
if ((i === tmpDate.getMonth()) && (j === (tmpDate.getDate() - 1))) {
break;
}
}
}
}
else if (countlyCommon.getPeriod() === "day") {
for (i = 0; i < tmpDate.getDate(); i++) {
_periodObj.currentPeriodArr.push(_periodObj.activePeriod + "." + (i + 1));
}
}
else {
_periodObj.currentPeriodArr.push(_periodObj.activePeriod);
}
}
for (i = 0; i < (_periodObj.currentPeriodArr.length); i++) {
dotSplit = _periodObj.currentPeriodArr[i].split(".");
tmpDateStr = "";
for (j = 0; j < dotSplit.length; j++) {
if (dotSplit[j].length === 1) {
tmpDateStr += "0" + dotSplit[j];
}
else {
tmpDateStr += dotSplit[j];
}
}
dateIds.push(tmpDateStr);
}
}
var tmpDateIds = [];
switch (bucket) {
case "hourly":
for (i = 0; i < 25; i++) {
tmpDateIds.push(dateIds[0] + ((i < 10) ? "0" + i : i));
}
dateIds = tmpDateIds;
break;
case "monthly":
for (i = 0; i < dateIds.length; i++) {
countlyCommon.arrayAddUniq(tmpDateIds, moment(dateIds[i], "YYYYMMDD").format("YYYYMM"));
}
dateIds = tmpDateIds;
break;
}
return dateIds;
};
countlyCommon.getNotesForDateId = function(dateId, appIdsForNotes) {
var ret = [];
var notes = [];
appIdsForNotes && appIdsForNotes.forEach(function(appId) {
if (countlyGlobal.apps[appId] && countlyGlobal.apps[appId].notes) {
notes = notes.concat(countlyGlobal.apps[appId].notes);
}
});
if (notes.length === 0) {
return ret;
}
for (var i = 0; i < notes.length; i++) {
if (!notes[i].dateId) {
notes[i].dateId = moment(notes[i].ts).format("YYYYMMDDHHmm");
}
if (notes[i].dateId.indexOf(dateId) === 0) {
ret = ret.concat([notes[i]]);
}
}
return ret;
};
/**
* Add item or array to existing array only if values are not already in original array. given array is modified.
* @memberof countlyCommon
* @param {array} arr - original array where to add unique elements
* @param {string|number|array} item - item to add or array to merge
*/
countlyCommon.arrayAddUniq = function(arr, item) {
if (!arr) {
arr = [];
}
if (toString.call(item) === "[object Array]") {
for (var i = 0; i < item.length; i++) {
if (arr.indexOf(item[i]) === -1) {
arr[arr.length] = item[i];
}
}
}
else {
if (arr.indexOf(item) === -1) {
arr[arr.length] = item;
}
}
};
/**
* Format timestamp to twitter like time ago format with real date as tooltip and hidden data for exporting
* @memberof countlyCommon
* @param {number} timestamp - timestamp in seconds or miliseconds
* @returns {string} formated time ago
* @example
* //outputs <span title="Tue, 17 Jan 2017 13:54:26">3 days ago<a style="display: none;">|Tue, 17 Jan 2017 13:54:26</a></span>
* countlyCommon.formatTimeAgo(1484654066);
*/
countlyCommon.formatTimeAgo = function(timestamp) {
var meta = countlyCommon.formatTimeAgoText(timestamp);
var elem = $("<span>");
elem.prop("title", meta.tooltip);
if (meta.color) {
elem.css("color", meta.color);
}
elem.text(meta.text);
elem.append("<a style='display: none;'>|" + meta.tooltip + "</a>");
return elem.prop('outerHTML');
};
/**
* Format timestamp to twitter like time ago format with real date as tooltip and hidden data for exporting
* @memberof countlyCommon
* @param {number} timestamp - timestamp in seconds or miliseconds
* @returns {string} formated time ago
* @example
* //outputs ago time without html tags
* countlyCommon.formatTimeAgo(1484654066);
*/
countlyCommon.formatTimeAgoText = function(timestamp) {
if (Math.round(timestamp).toString().length === 10) {
timestamp *= 1000;
}
var target = new Date(timestamp);
var tooltip = moment(target).format("ddd, D MMM YYYY HH:mm:ss");
var text = tooltip;
var color = null;
var now = new Date();
var diff = Math.floor((now - target) / 1000);
if (diff <= -2592000) {
return tooltip;
}
else if (diff < -86400) {
text = jQuery.i18n.prop("common.in.days", Math.abs(Math.round(diff / 86400)));
}
else if (diff < -3600) {
text = jQuery.i18n.prop("common.in.hours", Math.abs(Math.round(diff / 3600)));
}
else if (diff < -60) {
text = jQuery.i18n.prop("common.in.minutes", Math.abs(Math.round(diff / 60)));
}
else if (diff <= -1) {
color = "#50C354";
text = (jQuery.i18n.prop("common.in.seconds", Math.abs(diff)));
}
else if (diff <= 1) {
color = "#50C354";
text = jQuery.i18n.map["common.ago.just-now"];
}
else if (diff < 20) {
color = "#50C354";
text = jQuery.i18n.prop("common.ago.seconds-ago", diff);
}
else if (diff < 40) {
color = "#50C354";
text = jQuery.i18n.map["common.ago.half-minute"];
}
else if (diff < 60) {
color = "#50C354";
text = jQuery.i18n.map["common.ago.less-minute"];
}
else if (diff <= 90) {
text = jQuery.i18n.map["common.ago.one-minute"];
}
else if (diff <= 3540) {
text = jQuery.i18n.prop("common.ago.minutes-ago", Math.round(diff / 60));
}
else if (diff <= 5400) {
text = jQuery.i18n.map["common.ago.one-hour"];
}
else if (diff <= 86400) {
text = jQuery.i18n.prop("common.ago.hours-ago", Math.round(diff / 3600));
}
else if (diff <= 129600) {
text = jQuery.i18n.map["common.ago.one-day"];
}
else if (diff < 604800) {
text = jQuery.i18n.prop("common.ago.days-ago", Math.round(diff / 86400));
}
else if (diff <= 777600) {
text = jQuery.i18n.map["common.ago.one-week"];
}
else if (diff <= 2592000) {
text = jQuery.i18n.prop("common.ago.days-ago", Math.round(diff / 86400));
}
else {
text = tooltip;
}
return {
text: text,
tooltip: tooltip,
color: color
};
};
/**
* Format timestamp to D MMM YYYY, HH:mm
* @memberof countlyCommon
* @param {number} timestamp - timestamp in seconds or miliseconds
* @returns {string} formated time and date
* @example
* //outputs 16 Dec 2022, 12:16
* countlyCommon.formatTimeAndDateShort(1484654066);
*/
countlyCommon.formatTimeAndDateShort = function(timestamp) {
if (Math.round(timestamp).toString().length === 10) {
timestamp *= 1000;
}
return moment(new Date(timestamp)).format("D MMM YYYY, HH:mm");
};
/**
* Format duration to units of how much time have passed
* @memberof countlyCommon
* @param {number} timestamp - amount in seconds passed since some reference point
* @returns {string} formated time with how much units passed
* @example
* //outputs 47 year(s) 28 day(s) 11:54:26
* countlyCommon.formatTime(1484654066);
*/
countlyCommon.formatTime = function(timestamp) {
var str = "";
var seconds = timestamp % 60;
str = str + leadingZero(seconds);
timestamp -= seconds;
var minutes = timestamp % (60 * 60);
str = leadingZero(minutes / 60) + ":" + str;
timestamp -= minutes;
var hours = timestamp % (60 * 60 * 24);
str = leadingZero(hours / (60 * 60)) + ":" + str;
timestamp -= hours;
if (timestamp > 0) {
var days = timestamp % (60 * 60 * 24 * 365);
str = (days / (60 * 60 * 24)) + " day(s) " + str;
timestamp -= days;
if (timestamp > 0) {
str = (timestamp / (60 * 60 * 24 * 365)) + " year(s) " + str;
}
}
return str;
};
/**
* Format duration into highest unit of how much time have passed. Used in big numbers
* @memberof countlyCommon
* @param {number} timespent - amount in seconds passed since some reference point
* @returns {string} formated time with how much highest units passed
* @example
* //outputs 2824.7 yrs
* countlyCommon.timeString(1484654066);
*/
countlyCommon.timeString = function(timespent) {
var timeSpentString = (timespent.toFixed(1)) + " " + jQuery.i18n.map["common.minute.abrv"];
if (timespent >= 142560) {
timeSpentString = (timespent / 525600).toFixed(1) + " " + jQuery.i18n.map["common.year.abrv"];
}
else if (timespent >= 1440) {
timeSpentString = (timespent / 1440).toFixed(1) + " " + jQuery.i18n.map["common.day.abrv"];
}
else if (timespent >= 60) {
timeSpentString = (timespent / 60).toFixed(1) + " " + jQuery.i18n.map["common.hour.abrv"];
}
return timeSpentString;
/*var timeSpentString = "";
if(timespent > 1){
timeSpentString = Math.floor(timespent) + " " + jQuery.i18n.map["common.minute.abrv"]+" ";
var left = Math.floor((timespent - Math.floor(timespent))*60);
if(left > 0)
timeSpentString += left + " s";
}
else
timeSpentString += Math.floor((timespent - Math.floor(timespent))*60) + " s";
if (timespent >= 142560) {
timeSpentString = Math.floor(timespent / 525600) + " " + jQuery.i18n.map["common.year.abrv"];
var left = Math.floor((timespent - Math.floor(timespent / 525600)*525600)/1440);
if(left > 0)
timeSpentString += " "+left + " " + jQuery.i18n.map["common.day.abrv"];
} else if (timespent >= 1440) {
timeSpentString = Math.floor(timespent / 1440) + " " + jQuery.i18n.map["common.day.abrv"];
var left = Math.floor((timespent - Math.floor(timespent / 1440)*1440)/60);
if(left > 0)
timeSpentString += " "+left + " " + jQuery.i18n.map["common.hour.abrv"];
} else if (timespent >= 60) {
timeSpentString = Math.floor(timespent / 60) + " " + jQuery.i18n.map["common.hour.abrv"];
var left = Math.floor(timespent - Math.floor(timespent / 60)*60)
if(left > 0)
timeSpentString += " "+left + " " + jQuery.i18n.map["common.minute.abrv"];
}
return timeSpentString;*/
};
/**
* Get date from seconds timestamp
* @memberof countlyCommon
* @param {number} timestamp - timestamp in seconds or miliseconds
* @returns {string} formated date
* @example
* //outputs 17.01.2017
* countlyCommon.getDate(1484654066);
*/
countlyCommon.getDate = function(timestamp) {
if (Math.round(timestamp).toString().length === 10) {
timestamp *= 1000;
}
var d = new Date(timestamp);
return moment(d).format("ddd, D MMM YYYY");
//return leadingZero(d.getDate()) + "." + leadingZero(d.getMonth() + 1) + "." + d.getFullYear();
};
/**
* Get time from seconds timestamp
* @memberof countlyCommon
* @param {number} timestamp - timestamp in seconds or miliseconds
* @param {boolean} [showSeconds=false] - used to return seconds
* @returns {string} formated time
* @example
* //outputs 13:54
* countlyCommon.getTime(1484654066);
*/
countlyCommon.getTime = function(timestamp, showSeconds = false) {
if (Math.round(timestamp).toString().length === 10) {
timestamp *= 1000;
}
var d = new Date(timestamp);
var formattedTime = leadingZero(d.getHours()) + ":" + leadingZero(d.getMinutes());
if (showSeconds) {
formattedTime += ":" + leadingZero(d.getSeconds());
}
return formattedTime;
};
/**
* Round to provided number of digits
* @memberof countlyCommon
* @param {number} num - number to round
* @param {number} digits - amount of digits to round to
* @returns {number} rounded number
* @example
* //outputs 1.235
* countlyCommon.round(1.2345, 3);
*/
countlyCommon.round = function(num, digits) {
digits = Math.pow(10, digits || 0);
return Math.round(num * digits) / digits;
};
/**
* Get calculated totals for each property, usualy used as main dashboard data timeline data without metric segments
* @memberof countlyCommon
* @param {object} data - countly metric model data
* @param {array} properties - array of all properties to extract
* @param {array} unique - array of all properties that are unique from properties array. We need to apply estimation to them
* @param {object} estOverrideMetric - using unique property as key and total_users estimation property as value for all unique metrics that we want to have total user estimation overridden
* @param {function} clearObject - function to prefill all expected properties as u, t, n, etc with 0, so you would not have null in the result which won't work when drawing graphs
* @param {string=} segment - segment value for which to fetch metric data
* @returns {object} dashboard data object
* @example
* countlyCommon.getDashboardData(countlySession.getDb(), ["t", "n", "u", "d", "e", "p", "m"], ["u", "p", "m"], {u:"users"}, countlySession.clearObject);
* //outputs
* {
* "t":{"total":980,"prev-total":332,"change":"195.2%","trend":"u"},
* "n":{"total":402,"prev-total":255,"change":"57.6%","trend":"u"},
* "u":{"total":423,"prev-total":255,"change":"75.7%","trend":"u","isEstimate":false},
* "d":{"total":0,"prev-total":0,"change":"NA","trend":"u"},
* "e":{"total":980,"prev-total":332,"change":"195.2%","trend":"u"},
* "p":{"total":103,"prev-total":29,"change":"255.2%","trend":"u","isEstimate":true},
* "m":{"total":86,"prev-total":0,"change":"NA","trend":"u","isEstimate":true}
* }
*/
countlyCommon.getDashboardData = function(data, properties, unique, estOverrideMetric, clearObject, segment) {
if (segment) {
segment = "." + segment;
}
else {
segment = "";
}
var _periodObj = countlyCommon.periodObj,
dataArr = {},
tmp_x,
tmp_y,
tmpUniqObj,
tmpPrevUniqObj,
current = {},
previous = {},
currentCheck = {},
previousCheck = {},
change = {},
isEstimate = false;
var i = 0;
var j = 0;
for (i = 0; i < properties.length; i++) {
current[properties[i]] = 0;
previous[properties[i]] = 0;
currentCheck[properties[i]] = 0;
previousCheck[properties[i]] = 0;
}
if (_periodObj.isSpecialPeriod) {
isEstimate = true;
for (j = 0; j < (_periodObj.currentPeriodArr.length); j++) {
tmp_x = countlyCommon.getDescendantProp(data, _periodObj.currentPeriodArr[j] + segment);
tmp_x = clearObject(tmp_x);
for (i = 0; i < properties.length; i++) {
if (unique.indexOf(properties[i]) === -1) {
current[properties[i]] += tmp_x[properties[i]];
}
}
}
for (j = 0; j < (_periodObj.previousPeriodArr.length); j++) {
tmp_y = countlyCommon.getDescendantProp(data, _periodObj.previousPeriodArr[j] + segment);
tmp_y = clearObject(tmp_y);
for (i = 0; i < properties.length; i++) {
if (unique.indexOf(properties[i]) === -1) {
previous[properties[i]] += tmp_y[properties[i]];
}
}
}
//deal with unique values separately
for (j = 0; j < (_periodObj.uniquePeriodArr.length); j++) {
tmp_x = countlyCommon.getDescendantProp(data, _periodObj.uniquePeriodArr[j] + segment);
tmp_x = clearObject(tmp_x);
for (i = 0; i < unique.length; i++) {
current[unique[i]] += tmp_x[unique[i]];
}
}
for (j = 0; j < (_periodObj.previousUniquePeriodArr.length); j++) {
tmp_y = countlyCommon.getDescendantProp(data, _periodObj.previousUniquePeriodArr[j] + segment);
tmp_y = clearObject(tmp_y);
for (i = 0; i < unique.length; i++) {
previous[unique[i]] += tmp_y[unique[i]];
}
}
//recheck unique values with larger buckets
for (j = 0; j < (_periodObj.uniquePeriodCheckArr.length); j++) {
tmpUniqObj = countlyCommon.getDescendantProp(data, _periodObj.uniquePeriodCheckArr[j] + segment);
tmpUniqObj = clearObject(tmpUniqObj);
for (i = 0; i < unique.length; i++) {
currentCheck[unique[i]] += tmpUniqObj[unique[i]];
}
}
for (j = 0; j < (_periodObj.previousUniquePeriodArr.length); j++) {
tmpPrevUniqObj = countlyCommon.getDescendantProp(data, _periodObj.previousUniquePeriodArr[j] + segment);
tmpPrevUniqObj = clearObject(tmpPrevUniqObj);
for (i = 0; i < unique.length; i++) {
previousCheck[unique[i]] += tmpPrevUniqObj[unique[i]];
}
}
//check if we should overwrite uniques
for (i = 0; i < unique.length; i++) {
if (current[unique[i]] > currentCheck[unique[i]]) {
current[unique[i]] = currentCheck[unique[i]];
}
if (previous[unique[i]] > previousCheck[unique[i]]) {
previous[unique[i]] = previousCheck[unique[i]];
}
}
}
else {
tmp_x = countlyCommon.getDescendantProp(data, _periodObj.activePeriod + segment);
tmp_y = countlyCommon.getDescendantProp(data, _periodObj.previousPeriod + segment);
tmp_x = clearObject(tmp_x);
tmp_y = clearObject(tmp_y);
for (i = 0; i < properties.length; i++) {
current[properties[i]] = tmp_x[properties[i]];
previous[properties[i]] = tmp_y[properties[i]];
}
}
//check if we can correct data using total users correction
if (estOverrideMetric && countlyTotalUsers.isUsable()) {
for (i = 0; i < unique.length; i++) {
if (estOverrideMetric[unique[i]] && countlyTotalUsers.get(estOverrideMetric[unique[i]]).users) {
current[unique[i]] = countlyTotalUsers.get(estOverrideMetric[unique[i]]).users;
}
if (estOverrideMetric[unique[i]] && countlyTotalUsers.get(estOverrideMetric[unique[i]], true).users) {
previous[unique[i]] = countlyTotalUsers.get(estOverrideMetric[unique[i]], true).users;
}
}
}
// Total users can't be less than new users
if (typeof current.u !== "undefined" && typeof current.n !== "undefined" && current.u < current.n) {
if (estOverrideMetric && countlyTotalUsers.isUsable() && estOverrideMetric.u && countlyTotalUsers.get(estOverrideMetric.u).users) {
current.n = current.u;
}
else {
current.u = current.n;
}
}
// Total users can't be more than total sessions
if (typeof current.u !== "undefined" && typeof current.t !== "undefined" && current.u > current.t) {
current.u = current.t;
}
for (i = 0; i < properties.length; i++) {
change[properties[i]] = countlyCommon.getPercentChange(previous[properties[i]], current[properties[i]]);
dataArr[properties[i]] = {
"total": current[properties[i]],
"prev-total": previous[properties[i]],
"change": change[properties[i]].percent,
"trend": change[properties[i]].trend
};
if (unique.indexOf(properties[i]) !== -1) {
dataArr[properties[i]].isEstimate = isEstimate;
}
}
//check if we can correct data using total users correction
if (estOverrideMetric && countlyTotalUsers.isUsable()) {
for (i = 0; i < unique.length; i++) {
if (estOverrideMetric[unique[i]] && countlyTotalUsers.get(estOverrideMetric[unique[i]]).users) {
dataArr[unique[i]].isEstimate = false;
}
}
}
return dataArr;
};
/**
* Get total data for period's each time bucket as comma separated string to generate sparkle/small bar lines
* @memberof countlyCommon
* @param {object} data - countly metric model data
* @param {object} props - object where key is output property name and value could be string as key from data object or function to create new value based on existing ones
* @param {function} clearObject - function to prefill all expected properties as u, t, n, etc with 0, so you would not have null in the result which won't work when drawing graphs
* @returns {object} object with sparkleline data for each property
* @example
* var sparkLines = countlyCommon.getSparklineData(countlySession.getDb(), {
* "total-sessions": "t",
* "new-users": "n",
* "total-users": "u",
* "total-duration": "d",
* "events": "e",
* "returning-users": function(tmp_x){return Math.max(tmp_x["u"] - tmp_x["n"], 0);},
* "avg-duration-per-session": function(tmp_x){return (tmp_x["t"] == 0) ? 0 : (tmp_x["d"] / tmp_x["t"]);},
* "avg-events": function(tmp_x){return (tmp_x["u"] == 0) ? 0 : (tmp_x["e"] / tmp_x["u"]);}
* }, countlySession.clearObject);
* //outputs
* {
* "total-sessions":"73,84,80,72,61,18,11,7,17,27,66,39,41,36,39,36,6,11,6,16,22,30,33,34,32,41,29,9,2,2",
* "new-users":"24,30,25,20,16,18,11,7,17,18,20,18,17,11,15,15,6,11,6,16,13,14,12,10,7,4,8,9,2,2",
* "total-users":"45,54,50,44,37,18,11,7,17,27,36,39,41,36,39,36,6,11,6,16,22,30,33,34,32,29,29,9,2,2",
* "total-duration":"0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0",
* "events":"73,84,80,72,61,18,11,7,17,27,66,39,41,36,39,36,6,11,6,16,22,30,33,34,32,41,29,9,2,2",
* "returning-users":"21,24,25,24,21,0,0,0,0,9,16,21,24,25,24,21,0,0,0,0,9,16,21,24,25,25,21,0,0,0",
* "avg-duration-per-session":"0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0",
* "avg-events":"1.6222222222222222,1.5555555555555556,1.6,1.6363636363636365,1.6486486486486487,1,1,1,1,1,1.8333333333333333,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1.4137931034482758,1,1,1,1"
* }
*/
countlyCommon.getSparklineData = function(data, props, clearObject) {
var _periodObj = countlyCommon.periodObj;
var sparkLines = {};
for (var pp in props) {
sparkLines[pp] = [];
}
var tmp_x = "";
var i = 0;
var p = 0;
if (!_periodObj.isSpecialPeriod) {
for (i = _periodObj.periodMin; i < (_periodObj.periodMax + 1); i++) {
tmp_x = countlyCommon.getDescendantProp(data, _periodObj.activePeriod + "." + i);
tmp_x = clearObject(tmp_x);
for (p in props) {
if (typeof props[p] === "string") {
sparkLines[p].push(tmp_x[props[p]]);
}
else if (typeof props[p] === "function") {
sparkLines[p].push(props[p](tmp_x));
}
}
}
}
else {
for (i = 0; i < (_periodObj.currentPeriodArr.length); i++) {
tmp_x = countlyCommon.getDescendantProp(data, _periodObj.currentPeriodArr[i]);
tmp_x = clearObject(tmp_x);
for (p in props) {
if (typeof props[p] === "string") {
sparkLines[p].push(tmp_x[props[p]]);
}
else if (typeof props[p] === "function") {
sparkLines[p].push(props[p](tmp_x));
}
}
}
}
for (var key in sparkLines) {
sparkLines[key] = sparkLines[key].join(",");
}
return sparkLines;
};
/**
* Format date based on some locale settings
* @memberof countlyCommon
* @param {moment} date - moment js object
* @param {string} format - format string to use
* @returns {string} date in formatted string
* @example
* //outputs Jan 20
* countlyCommon.formatDate(moment(), "MMM D");
*/
countlyCommon.formatDate = function(date, format) {
format = countlyCommon.getDateFormat(format);
return date.format(format);
};
countlyCommon.getDateFormat = function(format) {
if (countlyCommon.BROWSER_LANG_SHORT.toLowerCase() === "ko") {
format = format.replace("MMM D", "MMM D[일]").replace("D MMM", "MMM D[일]");
}
else if (countlyCommon.BROWSER_LANG_SHORT.toLowerCase() === "ja") {
format = format
.replace("D MMM YYYY", "YYYY年 MMM D")
.replace("MMM D, YYYY", "YYYY年 MMM D")
.replace("D MMM, YYYY", "YYYY年 MMM D")
.replace("MMM YYYY", "YYYY年 MMM")
.replace("MMM D", "MMM D[日]")
.replace("D MMM", "MMM D[日]");
}
else if (countlyCommon.BROWSER_LANG_SHORT.toLowerCase() === "zh") {
format = format.replace("MMMM", "M").replace("MMM", "M").replace("MM", "M").replace("DD", "D").replace("D M, YYYY", "YYYY M D").replace("D M", "M D").replace("D", "D[日]").replace("M", "M[月]").replace("YYYY", "YYYY[年]");
}
return format;
};
countlyCommon.showTooltip = function(args) {
showTooltip(args);
};
/**
* Getter for period object
* @memberof countlyCommon
* @returns {object} returns {@link countlyCommon.periodObj}
*/
countlyCommon.getPeriodObj = function() {
if (countlyCommon.periodObj._period !== _period) {
countlyCommon.periodObj = calculatePeriodObject(_period);
}
return countlyCommon.periodObj;
};
/**
* Getter for period object by providing period string value
* @memberof countlyCommon
* @param {object} period - given period
* @param {number} currentTimeStamp timestamp
* @returns {object} returns {@link countlyCommon.periodObj}
*/
countlyCommon.calcSpecificPeriodObj = function(period, currentTimeStamp) {
return calculatePeriodObject(period, currentTimeStamp);
};
countlyCommon.calculateUniqueFromMap = function(dbObj, uniqueMap) {
var u = 0;
for (var year in uniqueMap) {
var yearVal = countlyCommon.getDescendantProp(dbObj, year) || {};
var calcYearVal = 0;
if (Object.keys(uniqueMap[year]).length > 0) {
for (var month in uniqueMap[year]) {
var ob = countlyCommon.getDescendantProp(dbObj, year + "." + month) || {};
var monthVal = ob.u || 0;
var calcMonthVal = 0;
if (Object.keys(uniqueMap[year][month]).length > 0) {
for (var week in uniqueMap[year][month]) {
var ob2 = countlyCommon.getDescendantProp(dbObj, year + "." + week) || {};
var weekVal = ob2.u || 0;
var calcWeekVal = 0;
for (var day in uniqueMap[year][month][week]) {
var ob3 = countlyCommon.getDescendantProp(dbObj, year + "." + month + "." + day) || {};
calcWeekVal += ob3.u || 0;
}
calcMonthVal += Math.min(weekVal, calcWeekVal);
}
}
else {
calcMonthVal = monthVal;
}
calcYearVal += Math.min(monthVal, calcMonthVal);
}
}
else {
calcYearVal = yearVal;
}
u += Math.min((yearVal.u || 0), calcYearVal);
}
return u;
};
/**
* Calculate period function
* @param {object} period - given period
* @param {number} currentTimestamp timestamp
* @returns {object} returns {@link countlyCommon.periodObj}
*/
function calculatePeriodObject(period, currentTimestamp) {
var startTimestamp, endTimestamp, periodObject, cycleDuration, nDays;
currentTimestamp = moment(currentTimestamp || undefined);
periodObject = {
activePeriod: undefined,
periodMax: undefined,
periodMin: undefined,
previousPeriod: undefined,
currentPeriodArr: [],
previousPeriodArr: [],
isSpecialPeriod: false,
dateString: undefined,
daysInPeriod: 0,
numberOfDays: 0, // new
uniquePeriodArr: [],
uniquePeriodCheckArr: [],
previousUniquePeriodArr: [],
previousUniquePeriodCheckArr: [],
periodContainsToday: true,
_period: period
};
endTimestamp = currentTimestamp.clone().endOf("day");
if (period && period.indexOf(",") !== -1) {
try {
period = JSON.parse(period);
}
catch (SyntaxError) {
period = "30days";
}
}
if (Array.isArray(period)) {
if ((period[0] + "").length === 10) {
period[0] *= 1000;
}
if ((period[1] + "").length === 10) {
period[1] *= 1000;
}
var fromDate, toDate;
if (Number.isInteger(period[0]) && Number.isInteger(period[1])) {
fromDate = moment(period[0]);
toDate = moment(period[1]);
}
else {
fromDate = moment(period[0], ["DD-MM-YYYY HH:mm:ss", "DD-MM-YYYY"]);
toDate = moment(period[1], ["DD-MM-YYYY HH:mm:ss", "DD-MM-YYYY"]);
}
startTimestamp = fromDate.clone().startOf("day");
endTimestamp = toDate.clone().endOf("day");
// fromDate.tz(_appTimezone);
// toDate.tz(_appTimezone);
if (fromDate.format("YYYY.M.D") === toDate.format("YYYY.M.D")) {
cycleDuration = moment.duration(1, "day");
Object.assign(periodObject, {
dateString: "D MMM, HH:mm",
periodMax: 23,
periodMin: 0,
activePeriod: fromDate.format("YYYY.M.D"),
currentPeriodArr: [fromDate.format("YYYY.M.D")],
previousPeriod: fromDate.clone().subtract(1, "day").format("YYYY.M.D")
});
}
else if (fromDate.valueOf() > toDate.valueOf()) {
//incorrect range - reset to 30 days
nDays = 30;
startTimestamp = currentTimestamp.clone().startOf("day").subtract(nDays - 1, "days");
endTimestamp = currentTimestamp.clone().endOf("day");
cycleDuration = moment.duration(nDays, "days");
Object.assign(periodObject, {
dateString: "D MMM",
isSpecialPeriod: true
});
}
else {
cycleDuration = moment.duration(Math.round(moment.duration(endTimestamp - startTimestamp).asDays()), "days");
Object.assign(periodObject, {
dateString: "D MMM",
isSpecialPeriod: true
});
}
}
else if (period === "month") {
startTimestamp = currentTimestamp.clone().startOf("year");
cycleDuration = moment.duration(1, "year");
periodObject.dateString = "MMM";
Object.assign(periodObject, {
dateString: "MMM",
periodMax: 12,
periodMin: 1,
activePeriod: currentTimestamp.year(),
previousPeriod: currentTimestamp.year() - 1
});
}
else if (period === "day") {
startTimestamp = currentTimestamp.clone().startOf("month");
cycleDuration = moment.duration(1, "month");
periodObject.dateString = "D MMM";
Object.assign(periodObject, {
dateString: "D MMM",
periodMax: currentTimestamp.clone().endOf("month").date(),
periodMin: 1,
activePeriod: currentTimestamp.format("YYYY.M"),
previousPeriod: currentTimestamp.clone().subtract(1, "month").format("YYYY.M")
});
}
else if (period === "prevMonth") {
startTimestamp = currentTimestamp.clone().subtract(1, "month").startOf("month");
endTimestamp = currentTimestamp.clone().subtract(1, "month").endOf("month");
cycleDuration = moment.duration(1, "month");
Object.assign(periodObject, {
dateString: "D MMM",
periodMax: currentTimestamp.clone().subtract(1, "month").endOf("month").date(),
periodMin: 1,
activePeriod: currentTimestamp.clone().subtract(1, "month").format("YYYY.M"),
previousPeriod: currentTimestamp.clone().subtract(2, "month").format("YYYY.M")
});
}
else if (period === "hour") {
startTimestamp = currentTimestamp.clone().startOf("day");
cycleDuration = moment.duration(1, "day");
Object.assign(periodObject, {
dateString: "HH:mm",
periodMax: 23,
periodMin: 0,
activePeriod: currentTimestamp.format("YYYY.M.D"),
previousPeriod: currentTimestamp.clone().subtract(1, "day").format("YYYY.M.D")
});
}
else if (period === "yesterday") {
var yesterday = currentTimestamp.clone().subtract(1, "day");
startTimestamp = yesterday.clone().startOf("day");
endTimestamp = yesterday.clone().endOf("day");
cycleDuration = moment.duration(1, "day");
Object.assign(periodObject, {
dateString: "D MMM, HH:mm",
periodMax: 23,
periodMin: 0,
activePeriod: yesterday.format("YYYY.M.D"),
previousPeriod: yesterday.clone().subtract(1, "day").format("YYYY.M.D")
});
}
else if (/([1-9][0-9]*)minutes/.test(period)) {
const nMinutes = parseInt(/([1-9][0-9]*)minutes/.exec(period)[1]);
startTimestamp = currentTimestamp.clone().startOf("minute").subtract(nMinutes - 1, "minutes");
cycleDuration = moment.duration(nMinutes, "minutes");
Object.assign(periodObject, {
dateString: "HH:mm",
isSpecialPeriod: true
});
}
else if (/([1-9][0-9]*)hours/.test(period)) {
const nHours = parseInt(/([1-9][0-9]*)hours/.exec(period)[1]);
startTimestamp = currentTimestamp.clone().startOf("hour").subtract(nHours - 1, "hours");
endTimestamp = currentTimestamp.clone().endOf("hour"),
cycleDuration = moment.duration(nHours, "hours");
Object.assign(periodObject, {
isHourly: true,
dateString: "D MMM, HH:mm",
isSpecialPeriod: true,
});
}
else if (/([1-9][0-9]*)days/.test(period)) {
nDays = parseInt(/([1-9][0-9]*)days/.exec(period)[1]);
startTimestamp = currentTimestamp.clone().startOf("day").subtract(nDays - 1, "days");
cycleDuration = moment.duration(nDays, "days");
Object.assign(periodObject, {
dateString: "D MMM",
isSpecialPeriod: true
});
}
else if (/([1-9][0-9]*)weeks/.test(period)) {
const nWeeks = parseInt(/([1-9][0-9]*)weeks/.exec(period)[1]);
startTimestamp = currentTimestamp.clone().startOf("week").subtract((nWeeks - 1), "weeks");
cycleDuration = moment.duration(currentTimestamp.clone().diff(startTimestamp)).asDays() + 1;
Object.assign(periodObject, {
dateString: "D MMM",
isSpecialPeriod: true
});
}
else if (/([1-9][0-9]*)months/.test(period)) {
const nMonths = parseInt(/([1-9][0-9]*)months/.exec(period)[1]);
startTimestamp = currentTimestamp.clone().startOf("month").subtract((nMonths - 1), "months");
cycleDuration = moment.duration(currentTimestamp.clone().diff(startTimestamp)).asDays() + 1;
Object.assign(periodObject, {
dateString: "D MMM",
isSpecialPeriod: true
});
}
else if (/([1-9][0-9]*)years/.test(period)) {
const nYears = parseInt(/([1-9][0-9]*)years/.exec(period)[1]);
startTimestamp = currentTimestamp.clone().startOf("year").subtract((nYears - 1), "years");
cycleDuration = moment.duration(currentTimestamp.clone().diff(startTimestamp)).asDays() + 1;
Object.assign(periodObject, {
dateString: "D MMM",
isSpecialPeriod: true
});
}
//incorrect period, defaulting to 30 days
else {
nDays = 30;
startTimestamp = currentTimestamp.clone().startOf("day").subtract(nDays - 1, "days");
cycleDuration = moment.duration(nDays, "days");
Object.assign(periodObject, {
dateString: "D MMM",
isSpecialPeriod: true
});
}
Object.assign(periodObject, {
start: startTimestamp.valueOf(),
end: endTimestamp.valueOf(),
daysInPeriod: Math.round(moment.duration(endTimestamp - startTimestamp).asDays()), //due to daylight saving time we might have 30 days and 1 hour, or 29 days and 23 hours between 2 dates
numberOfDays: Math.round(moment.duration(endTimestamp - startTimestamp).asDays()),
periodContainsToday: (startTimestamp <= currentTimestamp) && (currentTimestamp <= endTimestamp),
});
if (startTimestamp.weekYear() !== endTimestamp.weekYear()) {
Object.assign(periodObject, {
dateString: (periodObject.dateString + ", YYYY")
});
}
var uniqueMap = {};
var uniquePrevMap = {};
var date0 = startTimestamp.clone().format("YYYY.M.D");
date0 = date0.split(".");
var sY = date0[0];
var sM = date0[1];
var date1 = endTimestamp.clone().format("YYYY.M.D");
date1 = date1.split(".");
var eY = date1[0];
var eM = date1[1];
date0 = startTimestamp.clone().subtract(cycleDuration).format("YYYY.M.D");
date0 = date0.split(".");
var psY = date0[0];
var psM = date0[1];
date1 = endTimestamp.clone().subtract(cycleDuration).format("YYYY.M.D");
date1 = date1.split(".");
var peY = date1[0];
var peM = date1[1];
for (var dayIt = startTimestamp.clone(); dayIt < endTimestamp; dayIt.add(1, "day")) {
var dateVal = dayIt.format("YYYY.M.D");
var week = Math.ceil(dayIt.format("DDD") / 7);
dateVal = dateVal.split(".");
uniqueMap[dateVal[0]] = uniqueMap[dateVal[0]] || {};//each year
if (dateVal[0] === sY || dateVal[0] === eY) {
uniqueMap[dateVal[0]][dateVal[1]] = uniqueMap[dateVal[0]][dateVal[1]] || {}; //each month
if ((dateVal[0] === sY && dateVal[1] === sM) || (dateVal[0] === eY && dateVal[1] === eM)) {
uniqueMap[dateVal[0]][dateVal[1]]["w" + week] = uniqueMap[dateVal[0]][dateVal[1]]["w" + week] || {}; //each week
uniqueMap[dateVal[0]][dateVal[1]]["w" + week][dateVal[2]] = uniqueMap[dateVal[0]][dateVal[1]]["w" + week][dateVal[2]] || {}; //each day
}
}
if (!periodObject.isHourly) {
periodObject.currentPeriodArr.push(dayIt.format("YYYY.M.D"));
periodObject.previousPeriodArr.push(dayIt.clone().subtract(cycleDuration).format("YYYY.M.D"));
}
dateVal = dayIt.clone().subtract(cycleDuration).format("YYYY.M.D");
week = Math.ceil(dayIt.clone().subtract(cycleDuration).format("DDD") / 7);
dateVal = dateVal.split(".");
uniquePrevMap[dateVal[0]] = uniquePrevMap[dateVal[0]] || {};//each year
if (dateVal[0] === psY || dateVal[0] === peY) {
uniquePrevMap[dateVal[0]][dateVal[1]] = uniquePrevMap[dateVal[0]][dateVal[1]] || {}; //each month
if ((dateVal[0] === psY && dateVal[1] === psM) || (dateVal[0] === peY && dateVal[1] === peM)) {
uniquePrevMap[dateVal[0]][dateVal[1]]["w" + week] = uniquePrevMap[dateVal[0]][dateVal[1]]["w" + week] || {}; //each week
uniquePrevMap[dateVal[0]][dateVal[1]]["w" + week][dateVal[2]] = uniquePrevMap[dateVal[0]][dateVal[1]]["w" + week][dateVal[2]] || {}; //each day
}
}
}
if (periodObject.daysInPeriod === 1 && periodObject.currentPeriodArr && Array.isArray(periodObject.currentPeriodArr)) {
periodObject.activePeriod = periodObject.currentPeriodArr[0];
}
if (periodObject.isHourly) {
var startHour = startTimestamp.clone(),
endHour = endTimestamp.clone();
for (startHour; startHour < endHour; startHour.add(1, "hours")) {
periodObject.currentPeriodArr.push(startHour.format("YYYY.M.D.H"));
periodObject.previousPeriodArr.push(startHour.clone().subtract(cycleDuration).format("YYYY.M.D.H"));
}
}
var currentYear = 0,
currWeeksArr = [],
currWeekCounts = {},
currMonthsArr = [],
currMonthCounts = {},
currPeriodArr = [],
prevWeeksArr = [],
prevWeekCounts = {},
prevMonthsArr = [],
prevMonthCounts = {},
prevPeriodArr = [];
if (periodObject.daysInPeriod !== 0) {
for (var i = (periodObject.daysInPeriod - 1); i > -1; i--) {
var currIndex = moment(endTimestamp).subtract(i, 'days'),
currIndexYear = currIndex.year(),
prevIndex = moment(endTimestamp).subtract((periodObject.daysInPeriod + i), 'days'),
prevYear = prevIndex.year();
currentYear = currIndexYear;
// Current period variables
var currWeek = currentYear + "." + "w" + Math.ceil(currIndex.format("DDD") / 7);
currWeeksArr[currWeeksArr.length] = currWeek;
currWeekCounts[currWeek] = (currWeekCounts[currWeek]) ? (currWeekCounts[currWeek] + 1) : 1;
var currMonth = currIndex.format("YYYY.M");
currMonthsArr[currMonthsArr.length] = currMonth;
currMonthCounts[currMonth] = (currMonthCounts[currMonth]) ? (currMonthCounts[currMonth] + 1) : 1;
currPeriodArr[currPeriodArr.length] = currIndex.format("YYYY.M.D");
// Previous period variables
var prevWeek = prevYear + "." + "w" + Math.ceil(prevIndex.format("DDD") / 7);
prevWeeksArr[prevWeeksArr.length] = prevWeek;
prevWeekCounts[prevWeek] = (prevWeekCounts[prevWeek]) ? (prevWeekCounts[prevWeek] + 1) : 1;
var prevMonth = prevIndex.format("YYYY.M");
prevMonthsArr[prevMonthsArr.length] = prevMonth;
prevMonthCounts[prevMonth] = (prevMonthCounts[prevMonth]) ? (prevMonthCounts[prevMonth] + 1) : 1;
prevPeriodArr[prevPeriodArr.length] = prevIndex.format("YYYY.M.D");
}
}
periodObject.uniquePeriodArr = getUniqArray(currWeeksArr, currWeekCounts, currMonthsArr, currMonthCounts, currPeriodArr);
periodObject.uniquePeriodCheckArr = getUniqCheckArray(currWeeksArr, currWeekCounts, currMonthsArr, currMonthCounts);
periodObject.previousUniquePeriodArr = getUniqArray(prevWeeksArr, prevWeekCounts, prevMonthsArr, prevMonthCounts, prevPeriodArr);
periodObject.previousUniquePeriodCheckArr = getUniqCheckArray(prevWeeksArr, prevWeekCounts, prevMonthsArr, prevMonthCounts);
periodObject.uniqueMap = uniqueMap;
periodObject.uniquePrevMap = uniquePrevMap;
return periodObject;
}
var getPeriodObj = countlyCommon.getPeriodObj;
/** returns unique period check array
* @param {array} weeksArray_pd - weeks array
* @param {array} weekCounts_pd - week counts
* @param {array} monthsArray_pd - months array
* @param {array} monthCounts_pd - months counts
* @param {array} periodArr_pd - period array
* @returns {array} periods
*/
function getUniqArray(weeksArray_pd, weekCounts_pd, monthsArray_pd, monthCounts_pd, periodArr_pd) {
if (_period === "month" || _period === "day" || _period === "yesterday" || _period === "hour") {
return [];
}
if (Object.prototype.toString.call(_period) === '[object Array]' && _period.length === 2) {
if (_period[0] + 24 * 60 * 60 * 1000 >= _period[1]) {
return [];
}
}
var weeksArray = clone(weeksArray_pd),
weekCounts = clone(weekCounts_pd),
monthsArray = clone(monthsArray_pd),
monthCounts = clone(monthCounts_pd),
periodArr = clone(periodArr_pd);
var uniquePeriods = [],
tmpDaysInMonth = -1,
tmpPrevKey = -1,
rejectedWeeks = [],
rejectedWeekDayCounts = {};
var key = 0;
var i = 0;
for (key in weekCounts) {
// If this is the current week we can use it
if (key === moment().format("YYYY.\\w w").replace(" ", "")) {
continue;
}
if (weekCounts[key] < 7) {
for (i = 0; i < weeksArray.length; i++) {
weeksArray[i] = weeksArray[i].replace(key, 0);
}
}
}
for (key in monthCounts) {
if (tmpPrevKey !== key) {
if (moment().format("YYYY.M") === key) {
tmpDaysInMonth = moment().format("D");
}
else {
tmpDaysInMonth = moment(key, "YYYY.M").daysInMonth();
}
tmpPrevKey = key;
}
if (monthCounts[key] < tmpDaysInMonth) {
for (i = 0; i < monthsArray.length; i++) {
monthsArray[i] = monthsArray[i].replace(key, 0);
}
}
}
for (i = 0; i < monthsArray.length; i++) {
if (parseInt(monthsArray[i]) === 0) {
if (parseInt(weeksArray[i]) === 0 || (rejectedWeeks.indexOf(weeksArray[i]) !== -1)) {
uniquePeriods[i] = periodArr[i];
}
else {
uniquePeriods[i] = weeksArray[i];
}
}
else {
rejectedWeeks[rejectedWeeks.length] = weeksArray[i];
uniquePeriods[i] = monthsArray[i];
if (rejectedWeekDayCounts[weeksArray[i]]) {
rejectedWeekDayCounts[weeksArray[i]].count++;
}
else {
rejectedWeekDayCounts[weeksArray[i]] = {
count: 1,
index: i
};
}
}
}
var totalWeekCounts = _.countBy(weeksArray, function(per) {
return per;
});
for (var weekDayCount in rejectedWeekDayCounts) {
// If the whole week is rejected continue
if (rejectedWeekDayCounts[weekDayCount].count === 7) {
continue;
}
// If its the current week continue
if (moment().format("YYYY.\\w w").replace(" ", "") === weekDayCount && totalWeekCounts[weekDayCount] === rejectedWeekDayCounts[weekDayCount].count) {
continue;
}
// If only some part of the week is rejected we should add back daily buckets
var startIndex = rejectedWeekDayCounts[weekDayCount].index - (totalWeekCounts[weekDayCount] - rejectedWeekDayCounts[weekDayCount].count),
limit = startIndex + (totalWeekCounts[weekDayCount] - rejectedWeekDayCounts[weekDayCount].count);
for (i = startIndex; i < limit; i++) {
// If there isn't already a monthly bucket for that day
if (parseInt(monthsArray[i]) === 0) {
uniquePeriods[i] = periodArr[i];
}
}
}
rejectedWeeks = _.uniq(rejectedWeeks);
uniquePeriods = _.uniq(_.difference(uniquePeriods, rejectedWeeks));
return uniquePeriods;
}
/** returns unique period check array
* @param {array} weeksArray_pd - weeks array
* @param {array} weekCounts_pd - week counts
* @param {array} monthsArray_pd - months array
* @param {array} monthCounts_pd - months counts
* @returns {array} periods
*/
function getUniqCheckArray(weeksArray_pd, weekCounts_pd, monthsArray_pd, monthCounts_pd) {
if (_period === "month" || _period === "day" || _period === "yesterday" || _period === "hour") {
return [];
}
if (Object.prototype.toString.call(_period) === '[object Array]' && _period.length === 2) {
if (_period[0] + 24 * 60 * 60 * 1000 >= _period[1]) {
return [];
}
}
var weeksArray = clone(weeksArray_pd),
weekCounts = clone(weekCounts_pd),
monthsArray = clone(monthsArray_pd),
monthCounts = clone(monthCounts_pd);
var uniquePeriods = [],
tmpDaysInMonth = -1,
tmpPrevKey = -1;
var key = 0;
var i = 0;
for (key in weekCounts) {
if (key === moment().format("YYYY.\\w w").replace(" ", "")) {
continue;
}
if (weekCounts[key] < 1) {
for (i = 0; i < weeksArray.length; i++) {
weeksArray[i] = weeksArray[i].replace(key, 0);
}
}
}
for (key in monthCounts) {
if (tmpPrevKey !== key) {
if (moment().format("YYYY.M") === key) {
tmpDaysInMonth = moment().format("D");
}
else {
tmpDaysInMonth = moment(key, "YYYY.M").daysInMonth();
}
tmpPrevKey = key;
}
if (monthCounts[key] < (tmpDaysInMonth * 0.5)) {
for (i = 0; i < monthsArray.length; i++) {
monthsArray[i] = monthsArray[i].replace(key, 0);
}
}
}
for (i = 0; i < monthsArray.length; i++) {
if (parseInt(monthsArray[i]) === 0) {
if (parseInt(weeksArray[i]) !== 0) {
uniquePeriods[i] = weeksArray[i];
}
}
else {
uniquePeriods[i] = monthsArray[i];
}
}
uniquePeriods = _.uniq(uniquePeriods);
return uniquePeriods;
}
/** Function to clone object
* @param {object} obj - object to clone
* @returns {object} cloned object
*/
function clone(obj) {
if (null === obj || "object" !== typeof obj) {
return obj;
}
var copy = "";
if (obj instanceof Date) {
copy = new Date();
copy.setTime(obj.getTime());
return copy;
}
if (obj instanceof Array) {
copy = [];
for (var i = 0, len = obj.length; i < len; ++i) {
copy[i] = clone(obj[i]);
}
return copy;
}
if (obj instanceof Object) {
copy = {};
for (var attr in obj) {
if (Object.prototype.hasOwnProperty.call(obj, attr)) {
copy[attr] = clone(obj[attr]);
}
}
return copy;
}
}
/** Function to show the tooltip when any data point in the graph is hovered on.
* @param {object} args - tooltip info
* @param {number} args.x - x position
* @param {number} args.y- y position
* @param {string} args.contents - content for tooltip
* @param {string} args.title - title
* @param {string} args.notes - notes
*/
function showTooltip(args) {
var x = args.x || 0,
y = args.y || 0,
contents = args.contents,
title = args.title,
notes = args.notes;
var tooltip = $('<div id="graph-tooltip" class="v2"></div>').append('<span class="graph-tooltip-content">' + contents + '</span>');
if (title) {
tooltip.prepend('<span id="graph-tooltip-title">' + title + '</span>');
}
if (notes) {
var noteLines = (notes + "").split("===");
for (var i = 0; i < noteLines.length; i++) {
tooltip.append("<div class='note-line'>– " + noteLines[i] + "</div>");
}
}
$("#content").append("<div id='tooltip-calc'>" + $('<div>').append(tooltip.clone()).html() + "</div>");
var widthVal = $("#graph-tooltip").outerWidth(),
heightVal = $("#graph-tooltip").outerHeight();
$("#tooltip-calc").remove();
var newLeft = (x - (widthVal / 2)),
xReach = (x + (widthVal / 2));
if (notes) {
newLeft += 10.5;
xReach += 10.5;
}
if (xReach > $(window).width()) {
newLeft = (x - widthVal);
}
else if (xReach < 340) {
newLeft = x;
}
tooltip.css({
top: y - heightVal - 20,
left: newLeft
}).appendTo("body").show();
}
/** function adds leading zero to value.
* @param {number} value - given value
* @returns {string|number} fixed value
*/
function leadingZero(value) {
if (value > 9) {
return value;
}
return "0" + value;
}
/**
* Correct timezone offset on the timestamp for current browser's timezone
* @memberof countlyCommon
* @param {number} inTS - second or milisecond timestamp
* @returns {number} corrected timestamp applying user's timezone offset
*/
countlyCommon.getOffsetCorrectionForTimestamp = function(inTS) {
var intLength = Math.round(inTS).toString().length,
timeZoneOffset = new Date((intLength === 13) ? inTS : inTS * 1000).getTimezoneOffset(),
tzAdjustment = 0;
if (timeZoneOffset !== 0) {
if (intLength === 13) {
tzAdjustment = timeZoneOffset * 60000;
}
else if (intLength === 10) {
tzAdjustment = timeZoneOffset * 60;
}
}
return tzAdjustment;
};
var __months = [];
/**
* Get array of localized short month names from moment js
* @memberof countlyCommon
* @param {boolean} reset - used to reset months cache when changing locale
* @returns {array} array of short localized month names used in moment js MMM formatting
* @example
* //outputs ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
* countlyCommon.getMonths();
*/
countlyCommon.getMonths = function(reset) {
if (reset) {
__months = [];
}
if (!__months.length) {
for (var i = 0; i < 12; i++) {
__months.push(moment.localeData().monthsShort(moment([0, i]), ""));
}
}
return __months;
};
/**
* Currently selected period
* @memberof countlyCommon
* @property {array=} currentPeriodArr - array with ticks for current period (available only for special periods), example ["2016.12.22","2016.12.23","2016.12.24", ...]
* @property {array=} previousPeriodArr - array with ticks for previous period (available only for special periods), example ["2016.12.22","2016.12.23","2016.12.24", ...]
* @property {string} dateString - date format to use when outputting date in graphs, example D MMM, YYYY
* @property {boolean} isSpecialPeriod - true if current period is special period, false if it is not
* @property {number} daysInPeriod - amount of full days in selected period, example 30
* @property {number} numberOfDays - number of days selected period consists of, example hour period has 1 day
* @property {boolean} periodContainsToday - true if period contains today, false if not
* @property {array} uniquePeriodArr - array with ticks for current period which contains data for unique values, like unique users, example ["2016.12.22","2016.w52","2016.12.30", ...]
* @property {array} uniquePeriodCheckArr - array with ticks for higher buckets to current period unique value estimation, example ["2016.w51","2016.w52","2016.w53","2017.1",...]
* @property {array} previousUniquePeriodArr - array with ticks for previous period which contains data for unique values, like unique users, example ["2016.12.22","2016.w52","2016.12.30"]
* @property {array} previousUniquePeriodCheckArr - array with ticks for higher buckets to previous period unique value estimation, example ["2016.w47","2016.w48","2016.12"]
* @property {string} activePeriod - period name formatted in dateString
* @property {string} previousPeriod - previous period name formatted in dateString
* @property {number} periodMax - max value of current period tick
* @property {number} periodMin - min value of current period tick
* @example <caption>Special period object (7days)</caption>
* {
* "currentPeriodArr":["2017.1.14","2017.1.15","2017.1.16","2017.1.17","2017.1.18","2017.1.19","2017.1.20"],
* "previousPeriodArr":["2017.1.7","2017.1.8","2017.1.9","2017.1.10","2017.1.11","2017.1.12","2017.1.13"],
* "isSpecialPeriod":true,
* "dateString":"D MMM",
* "daysInPeriod":7,
* "numberOfDays":7,
* "uniquePeriodArr":["2017.1.14","2017.w3"],
* "uniquePeriodCheckArr":["2017.w2","2017.w3"],
* "previousUniquePeriodArr":["2017.1.7","2017.1.8","2017.1.9","2017.1.10","2017.1.11","2017.1.12","2017.1.13"],
* "previousUniquePeriodCheckArr":["2017.w1","2017.w2"],
* "periodContainsToday":true
* }
* @example <caption>Simple period object (today period - hour)</caption>
* {
* "activePeriod":"2017.1.20",
* "periodMax":23,
* "periodMin":0,
* "previousPeriod":"2017.1.19",
* "isSpecialPeriod":false,
* "dateString":"HH:mm",
* "daysInPeriod":0,
* "numberOfDays":1,
* "uniquePeriodArr":[],
* "uniquePeriodCheckArr":[],
* "previousUniquePeriodArr":[],
* "previousUniquePeriodCheckArr":[],
* "periodContainsToday":true
* }
*/
countlyCommon.periodObj = calculatePeriodObject();
/**
* Parse second to standard time format
* @memberof countlyCommon
* @param {number} second number
* @param {number} [trimTo=5] number [1,5]
* @returns {string} return format "Xh Xm Xs", if trimTo is specified the length of the result is trimmed
* @example trimTo = 2, "Xh Xm Xs" result will be trimmed to "Xh Xm"
*/
countlyCommon.formatSecond = function(second, trimTo = 5) {
var timeLeft = parseFloat(second);
var dict = [
{k: 'year', v: 31536000},
{k: 'day', v: 86400},
{k: 'hour', v: 3600},
{k: 'minute', v: 60},
{k: 'second', v: 1}
];
var result = {year: 0, day: 0, hour: 0, minute: 0, second: 0};
var resultStrings = [];
for (var i = 0; i < dict.length && resultStrings.length < 3; i++) {
if (dict[i].k === "second") {
if (timeLeft < 0.1) {
result.second = 0;
}
else {
result.second = Math.round(timeLeft * 10) / 10;
}
}
else {
result[dict[i].k] = Math.floor(timeLeft / dict[i].v);
}
timeLeft = timeLeft % dict[i].v;
if (result[dict[i].k] > 0) {
if (result[dict[i].k] === 1) {
resultStrings.push(result[dict[i].k] + "" + jQuery.i18n.map["common." + dict[i].k + ".abrv2"]);
}
else {
resultStrings.push(result[dict[i].k] + "" + jQuery.i18n.map["common." + dict[i].k + ".abrv"]);
}
}
}
if (resultStrings.length === 0) {
return "0";
}
else {
if (trimTo > 5 || trimTo < 1) {
trimTo = 5;
}
return (resultStrings.slice(0, Math.min(trimTo, resultStrings.length))).join(' ');
}
};
/**
* add one more column in chartDP[index].data to show string in dp
* @memberof countlyCommon
* @param {array} chartDPs - chart data points
* @param {string} labelName - label name
* @return {array} chartDPs
* @example
* for example:
* chartDPs = [
* {color:"#88BBC8", label:"duration", data:[[0, 23], [1, 22]}],
* {color:"#88BBC8", label:"count", data:[[0, 3], [1, 3]}],
* }
* lable = 'duration',
*
* will return
* chartDPs = [
* {color:"#88BBC8", label:"duration", data:[[0, 23, "00:00:23"], [1, 22, "00:00:22"]}],
* {color:"#88BBC8", label:"count", data:[[0, 3], [1, 3]}],
* }
*/
countlyCommon.formatSecondForDP = function(chartDPs, labelName) {
for (var k = 0; k < chartDPs.length; k++) {
if (chartDPs[k].label === labelName) {
var dp = chartDPs[k];
for (var i = 0; i < dp.data.length; i++) {
dp.data[i][2] = countlyCommon.formatSecond(dp.data[i][1]);
}
}
}
return chartDPs;
};
/**
* Getter/setter for dot notatons:
* @memberof countlyCommon
* @param {object} obj - object to use
* @param {string} is - path of properties to get
* @param {varies} value - value to set
* @returns {varies} value at provided path
* @example
* common.dot({a: {b: {c: 'string'}}}, 'a.b.c') === 'string'
* common.dot({a: {b: {c: 'string'}}}, ['a', 'b', 'c']) === 'string'
* common.dot({a: {b: {c: 'string'}}}, 'a.b.c', 5) === 5
* common.dot({a: {b: {c: 'string'}}}, 'a.b.c') === 5
*/
countlyCommon.dot = function(obj, is, value) {
if (typeof is === 'string') {
return countlyCommon.dot(obj, is.split('.'), value);
}
else if (is.length === 1 && value !== undefined) {
obj[is[0]] = value;
return value;
}
else if (is.length === 0) {
return obj;
}
else if (!obj) {
return obj;
}
else {
if (typeof obj[is[0]] === "undefined" && value !== undefined) {
obj[is[0]] = {};
}
return countlyCommon.dot(obj[is[0]], is.slice(1), value);
}
};
/**
* Save division, handling division by 0 and rounding up to 2 decimals
* @memberof countlyCommon
* @param {number} dividend - object to use
* @param {number} divisor - path of properties to get
* @returns {number} division
*/
countlyCommon.safeDivision = function(dividend, divisor) {
var tmpAvgVal;
tmpAvgVal = dividend / divisor;
if (!tmpAvgVal || tmpAvgVal === Number.POSITIVE_INFINITY) {
tmpAvgVal = 0;
}
return tmpAvgVal.toFixed(2);
};
/**
* Returns a string with a language-sensitive representation of this number.
* @memberof countlyCommon
* @param {string} value - expected value to be formatted
* @param {number} currencyVal - expected currency to be formatted
* @returns {string} formatted value
*/
countlyCommon.numberToLocaleString = function(value, currencyVal) {
if (!value) {
return 0;
}
if (typeof value !== 'number') {
value = countlyCommon.localeStringToNumber(value);
}
return value.toLocaleString('en-US', { currency: currencyVal || "USD" });
};
/**
* Formats and returns local string to number
* @memberof countlyCommon
* @param {string} localeString - expected value to be formatted
* @returns {number} formatted value
*/
countlyCommon.localeStringToNumber = function(localeString) {
var number = null, fractionFloat;
if (localeString && typeof localeString === "string") {
var isContainDot = localeString.includes('.');
if (isContainDot) {
if (localeString.split('.')[1].length) {
var fractionString = localeString.split('.')[1];
var fractionNumber = parseInt(fractionString);
var pow = fractionString.length;
fractionFloat = fractionNumber / Math.pow(10, pow);
}
else {
fractionFloat = 0.00;
}
number = parseFloat(localeString.split('.')[0].replaceAll(',', '')) + fractionFloat;
}
else {
number = parseInt(localeString.replaceAll(',', ''));
}
return number;
}
return localeString;
};
/**
* Get timestamp range in format as [startTime, endTime] with period and base time
* @memberof countlyCommon
* @param {object} period - period has two format: array or string
* @param {number} baseTimeStamp - base timestamp to calc the period range
* @returns {array} period range
*/
countlyCommon.getPeriodRange = function(period, baseTimeStamp) {
var periodRange;
period = period || countlyCommon.DEFAULT_PERIOD;
var excludeCurrentDay = false;
if (period.period) {
excludeCurrentDay = period.exclude_current_day || false;
period = period.period;
}
var start;
var endTimeStamp = excludeCurrentDay ? moment(baseTimeStamp).subtract(1, 'day').hour(23).minute(59).second(59).toDate().getTime() : baseTimeStamp;
if (period.since) {
period = [period.since, endTimeStamp];
}
else if (period.indexOf(",") !== -1) {
try {
period = JSON.parse(period);
}
catch (SyntaxError) {
period = countlyCommon.DEFAULT_PERIOD;
}
}
if (Object.prototype.toString.call(period) === '[object Array]' && period.length === 2) { //range
periodRange = [period[0] + countlyCommon.getOffsetCorrectionForTimestamp(period[0]), period[1] + countlyCommon.getOffsetCorrectionForTimestamp(period[1])];
return periodRange;
}
switch (period) {
case 'hour':
start = moment(baseTimeStamp).hour(0).minute(0).second(0);
break;
case 'yesterday':
start = moment(baseTimeStamp).subtract(1, 'day').hour(0).minute(0).second(0);
endTimeStamp = moment(baseTimeStamp).subtract(1, 'day').hour(23).minute(59).second(59).toDate().getTime();
break;
case 'day':
start = moment(baseTimeStamp).date(1).hour(0).minute(0).second(0);
break;
case 'prevMonth':
start = moment(baseTimeStamp).subtract(1, "month").date(1).hour(0).minute(0).second(0);
endTimeStamp = moment(baseTimeStamp).subtract(1, "month").endOf('month').hour(23).minute(59).second(59).toDate().getTime();
break;
case 'month':
start = moment(baseTimeStamp).month(0).date(1).hour(0).minute(0).second(0);
break;
default:
if (/([1-9][0-9]*)days/.test(period)) {
let nDays = parseInt(/([1-9][0-9]*)days/.exec(period)[1]);
start = moment(baseTimeStamp).startOf("day").subtract(nDays - 1, "days");
}
else if (/([1-9][0-9]*)weeks/.test(period)) {
const nWeeks = parseInt(/([1-9][0-9]*)weeks/.exec(period)[1]);
start = moment(baseTimeStamp).startOf("week").subtract((nWeeks - 1), "weeks");
}
else if (/([1-9][0-9]*)months/.test(period)) {
const nMonths = parseInt(/([1-9][0-9]*)months/.exec(period)[1]);
start = moment(baseTimeStamp).startOf("month").subtract((nMonths - 1), "months");
}
else if (/([1-9][0-9]*)years/.test(period)) {
const nYears = parseInt(/([1-9][0-9]*)years/.exec(period)[1]);
start = moment(baseTimeStamp).startOf("year").subtract((nYears - 1), "years");
}
//incorrect period, defaulting to 30 days
else {
let nDays = 30;
start = moment(baseTimeStamp).startOf("day").subtract(nDays - 1, "days");
}
}
periodRange = [start.toDate().getTime(), endTimeStamp];
return periodRange;
};
/*
fast-levenshtein - Levenshtein algorithm in Javascript
(MIT License) Copyright (c) 2013 Ramesh Nair
https://github.com/hiddentao/fast-levenshtein
*/
var collator;
try {
collator = (typeof Intl !== "undefined" && typeof Intl.Collator !== "undefined") ? Intl.Collator("generic", { sensitivity: "base" }) : null;
}
catch (err) {
// console.log("Failed to initialize collator for Levenshtein\n" + err.stack);
}
// arrays to re-use
var prevRow = [],
str2Char = [];
/**
* Based on the algorithm at http://en.wikipedia.org/wiki/Levenshtein_distance.
* @memberof countlyCommon
*/
countlyCommon.Levenshtein = {
/**
* Calculate levenshtein distance of the two strings.
*
* @param {string} str1 String the first string.
* @param {string} str2 String the second string.
* @param {object} [options] Additional options.
* @param {boolean} [options.useCollator] Use `Intl.Collator` for locale-sensitive string comparison.
* @return {number} Integer the levenshtein distance (0 and above).
*/
get: function(str1, str2, options) {
var useCollator = (options && collator && options.useCollator);
var str1Len = str1.length,
str2Len = str2.length;
// base cases
if (str1Len === 0) {
return str2Len;
}
if (str2Len === 0) {
return str1Len;
}
// two rows
var curCol, nextCol, i, j, tmp;
// initialise previous row
for (i = 0; i < str2Len; ++i) {
prevRow[i] = i;
str2Char[i] = str2.charCodeAt(i);
}
prevRow[str2Len] = str2Len;
var strCmp;
if (useCollator) {
// calculate current row distance from previous row using collator
for (i = 0; i < str1Len; ++i) {
nextCol = i + 1;
for (j = 0; j < str2Len; ++j) {
curCol = nextCol;
// substution
strCmp = 0 === collator.compare(str1.charAt(i), String.fromCharCode(str2Char[j]));
nextCol = prevRow[j] + (strCmp ? 0 : 1);
// insertion
tmp = curCol + 1;
if (nextCol > tmp) {
nextCol = tmp;
}
// deletion
tmp = prevRow[j + 1] + 1;
if (nextCol > tmp) {
nextCol = tmp;
}
// copy current col value into previous (in preparation for next iteration)
prevRow[j] = curCol;
}
// copy last col value into previous (in preparation for next iteration)
prevRow[j] = nextCol;
}
}
else {
// calculate current row distance from previous row without collator
for (i = 0; i < str1Len; ++i) {
nextCol = i + 1;
for (j = 0; j < str2Len; ++j) {
curCol = nextCol;
// substution
strCmp = str1.charCodeAt(i) === str2Char[j];
nextCol = prevRow[j] + (strCmp ? 0 : 1);
// insertion
tmp = curCol + 1;
if (nextCol > tmp) {
nextCol = tmp;
}
// deletion
tmp = prevRow[j + 1] + 1;
if (nextCol > tmp) {
nextCol = tmp;
}
// copy current col value into previous (in preparation for next iteration)
prevRow[j] = curCol;
}
// copy last col value into previous (in preparation for next iteration)
prevRow[j] = nextCol;
}
}
return nextCol;
}
};
countlyCommon.getNotesPopup = function(dateId, appIds) {
var notes = countlyCommon.getNotesForDateId(dateId, appIds);
var dialog = $("#cly-popup").clone().removeAttr("id").addClass('graph-notes-popup');
dialog.removeClass('black');
var content = dialog.find(".content");
var notesPopupHTML = Handlebars.compile($("#graph-notes-popup").html());
notes.forEach(function(n) {
n.ts_display = moment(n.ts).format("D MMM, YYYY, HH:mm");
var app = countlyGlobal.apps[n.app_id] || {};
n.app_name = app.name;
});
var noteDateFormat = "D MMM, YYYY";
if (countlyCommon.getPeriod() === "month") {
noteDateFormat = "MMM YYYY";
}
var notePopupTitleTime = moment(notes[0].ts).format(noteDateFormat);
content.html(notesPopupHTML({notes: notes, notePopupTitleTime: notePopupTitleTime}));
CountlyHelpers.revealDialog(dialog);
$(".close-note-popup-button").off("click").on("click", function() {
CountlyHelpers.removeDialog(dialog);
});
window.app.localize();
};
countlyCommon.getGraphNotes = function(appIds, filter, callBack) {
if (!appIds) {
appIds = [];
}
var args = {
"app_id": countlyCommon.ACTIVE_APP_ID,
"notes_apps": JSON.stringify(appIds),
"period": JSON.stringify([countlyCommon.periodObj.start, countlyCommon.periodObj.end]),
"method": "notes",
"dt": Date.now()
};
if (filter && filter.noteType) {
args.note_type = filter.noteType;
}
if (filter && filter.category) {
args.category = JSON.stringify(filter.category);
}
if (filter && filter.customPeriod) {
args.period = JSON.stringify(filter.customPeriod);
}
return window.$.ajax({
type: "GET",
url: countlyCommon.API_PARTS.data.r,
data: args,
success: function(json) {
var notes = json && json.aaData || [];
var noteSortByApp = {};
notes.forEach(function(note) {
if (!noteSortByApp[note.app_id]) {
noteSortByApp[note.app_id] = [];
}
noteSortByApp[note.app_id].push(note);
});
appIds.forEach(function(appId) {
if (window.countlyGlobal.apps[appId]) {
window.countlyGlobal.apps[appId].notes = noteSortByApp[appId] || [];
}
});
callBack && callBack(notes);
}
});
};
/**
* Compare two versions
* @memberof countlyCommon
* @param {String} a, First version
* @param {String} b, Second version
* @returns {Number} returns -1, 0 or 1 by result of comparing
*/
countlyCommon.compareVersions = function(a, b) {
var aParts = a.split('.');
var bParts = b.split('.');
for (var j = 0; j < aParts.length && j < bParts.length; j++) {
var aPartNum = parseInt(aParts[j], 10);
var bPartNum = parseInt(bParts[j], 10);
var cmp = Math.sign(aPartNum - bPartNum);
if (cmp !== 0) {
return cmp;
}
}
if (aParts.length === bParts.length) {
return 0;
}
var longestArray = aParts;
if (bParts.length > longestArray.length) {
longestArray = bParts;
}
var continueIndex = Math.min(aParts.length, bParts.length);
for (var i = continueIndex; i < longestArray.length; i += 1) {
if (parseInt(longestArray[i], 10) > 0) {
return longestArray === bParts ? -1 : +1;
}
}
return 0;
};
/**
* Converts cohort time period to string.
* @param {Object} obj Inferred time object. Must contain "value", "type" and optionally "level".
* @returns {Object} String fields
*/
countlyCommon.getTimePeriodDescriptions = function(obj) {
if (obj.type === "all-time") {
return { name: jQuery.i18n.map['common.all-time'], valueAsString: "0days" };
}
if (obj.type === "last-n") {
var level = obj.level || "days";
return {
name: jQuery.i18n.prop('common.in-last-' + level + (obj.value > 1 ? '-plural' : ''), obj.value),
valueAsString: obj.value + level
};
}
if (obj.type === "hour") {
return {
name: jQuery.i18n.map["common.today"],
valueAsString: "hour"
};
}
if (obj.type === "yesterday") {
return {
name: jQuery.i18n.map["common.yesterday"],
valueAsString: "yesterday"
};
}
if (obj.type === "day") {
return {
name: moment().format("MMMM, YYYY"),
valueAsString: "day"
};
}
if (obj.type === "prevMonth") {
return {
name: moment().subtract(1, "month").format("MMMM, YYYY"),
valueAsString: "prevMonth"
};
}
if (obj.type === "month") {
return {
name: moment().year(),
valueAsString: "month"
};
}
var valueAsString = JSON.stringify(obj.value);
var name = valueAsString;
var formatDate = function(point, isShort) {
var format = "MMMM DD, YYYY";
if (isShort) {
format = "MMM DD, YYYY";
}
if (point.toString().length === 10) {
point *= 1000;
}
return countlyCommon.formatDate(moment(point), format);
};
if (Array.isArray(obj.value)) {
name = jQuery.i18n.prop('common.time-period-name.range', formatDate(obj.value[0], true), formatDate(obj.value[1], true));
}
else {
name = jQuery.i18n.prop('common.time-period-name.' + obj.type, formatDate(obj.value[obj.type]));
}
return {
name: name,
valueAsString: valueAsString
};
};
/**
* Cohort time period is a string (may still contain an array or an object). The needed
* meta data, however, is not included within the field. This function infers the meta data
* and returns as an object. Meta data is not persisted in db, just used in the UI.
*
* Example:
*
* Input: "[1561928400,1595203200]"
*
* // Other input forms:
* // "0days" (All Time)
* // "10days", "10weeks", etc. (In the Last)
* // "[1561928400,1595203200]" (In Between)
* // "{'on':1561928400}" (On)
* // "{'since':1561928400}" (Since)
*
* Output:
* {
* level: "days" // only effective when the type is "last-n"
* longName: "Jul 01, 2019-Jul 20, 2020"
* name: "Jul 01, 2019-Jul 20, 2020"
* type: "range"
* value: [1561928400, 1595203200]
* valueAsString: "[1561928400,1595203200]"
* }
*
* @param {string} period Period string
* @returns {Object} An object containing meta fields
*/
countlyCommon.convertToTimePeriodObj = function(period) {
var inferredLevel = "days",
inferredType = null,
inferredValue = null;
if (typeof period === "string" && (period.indexOf("{") > -1 || period.indexOf("[") > -1)) {
period = JSON.parse(period);
}
if (!period && period === 0) {
inferredType = "all-time";
inferredValue = 0;
}
else if (Array.isArray(period)) {
inferredType = "range";
}
else if (period === "hour") {
inferredType = "hour";
inferredValue = "hour";
}
else if (period === "yesterday") {
inferredType = "yesterday";
inferredValue = "yesterday";
}
else if (period === "day") {
inferredType = "day";
inferredValue = "day";
}
else if (period === "prevMonth") {
inferredType = "prevMonth";
inferredValue = "prevMonth";
}
else if (period === "month") {
inferredType = "month";
inferredValue = "month";
}
else if (typeof period === "object") {
if (Object.prototype.hasOwnProperty.call(period, "since")) {
inferredType = "since";
}
else if (Object.prototype.hasOwnProperty.call(period, "on")) {
inferredType = "on";
}
else if (Object.prototype.hasOwnProperty.call(period, "before")) {
inferredType = "before";
}
}
else if (period.endsWith("minutes")) {
inferredLevel = "minutes";
inferredType = "last-n";
}
else if (period.endsWith("hours")) {
inferredLevel = "hours";
inferredType = "last-n";
}
else if (period.endsWith("days")) {
inferredLevel = "days";
inferredType = "last-n";
}
else if (period.endsWith("weeks")) {
inferredLevel = "weeks";
inferredType = "last-n";
}
else if (period.endsWith("months")) {
inferredLevel = "months";
inferredType = "last-n";
}
else if (period.endsWith('years')) {
inferredLevel = 'years';
inferredType = 'last-n';
}
else {
inferredType = "all-time";
inferredValue = 0;
}
if (inferredValue !== 0 && inferredType === "last-n") {
inferredValue = parseInt((period.replace(inferredLevel, '')));
}
else if (inferredValue !== 0) {
var stringified = JSON.stringify(period);
inferredValue = JSON.parse(stringified);
}
var obj = {
value: inferredValue,
type: inferredType,
level: inferredLevel
};
var descriptions = countlyCommon.getTimePeriodDescriptions(obj);
obj.valueAsString = descriptions.valueAsString;
obj.name = obj.longName = descriptions.name;
return obj;
};
/**
* Function to change HEX to RGBA
* @param {String} h - hex code
* @returns {String} rgba string
*/
countlyCommon.hexToRgba = function(h) {
var r = 0, g = 0, b = 0, a = 1;
if (h.length === 4) {
r = "0x" + h[1] + h[1];
g = "0x" + h[2] + h[2];
b = "0x" + h[3] + h[3];
}
else if (h.length === 7) {
r = "0x" + h[1] + h[2];
g = "0x" + h[3] + h[4];
b = "0x" + h[5] + h[6];
}
return "rgba(" + +r + "," + +g + "," + +b + "," + a + ")";
};
/**
* Unescapes provided string.
* -- Please use carefully --
* Mainly for rendering purposes.
* @param {String} text - Arbitrary string
* @param {String} df - Default value
* @returns {String} rgba string
*/
countlyCommon.unescapeString = function(text, df) {
if (text === undefined && df === undefined) {
return undefined;
}
return _.unescape(text || df).replace(/'/g, "'");
};
/**
* Remove spaces, tabs, and newlines from the start and end of the string
* @param {String} str - Arbitrary string
* @returns {String} Trimmed string
*/
countlyCommon.trimWhitespaceStartEnd = function(str) {
if (typeof str !== 'string') {
return str;
}
str = str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
return str;
};
};
window.CommonConstructor = CommonConstructor;
window.countlyCommon = new CommonConstructor();
}(window, jQuery));