Skip to content

Commit 26e9038

Browse files
authored
Merge pull request #710 from melihkorkmaz/loyalyt-chart
Loyalyt chart
2 parents 1ade44a + ff8e2c2 commit 26e9038

File tree

4 files changed

+239
-24
lines changed

4 files changed

+239
-24
lines changed

api/parts/mgmt/app_users.js

+72
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,78 @@ usersApi.export = function(app_id, query, params, callback) {
963963
});
964964
};
965965

966+
/**
967+
* Fetch aggregated data from DB.
968+
* @param {string} collectionName | Name of collection
969+
* @param {object} aggregation | Aggregation object
970+
* @return {object} | new promise
971+
*/
972+
const getAggregatedAppUsers = (collectionName, aggregation) => {
973+
return new Promise((resolve, reject) => {
974+
common.db.collection(collectionName).aggregate(aggregation, (err, result) => {
975+
if (err) {
976+
reject(err);
977+
return;
978+
}
979+
resolve(result);
980+
});
981+
});
982+
};
983+
984+
usersApi.loyalty = function(params) {
985+
const rangeLabels = ["1", "2", "3-5", "6-9", "10-19", "20-49", "50-99", "100-499", "> 500"];
986+
const ranges = [[1], [2], [3, 5], [6, 9], [10, 19], [20, 49], [40, 99], [100, 499], [500] ];
987+
const collectionName = 'app_users' + params.qstring.app_id;
988+
989+
// Time
990+
const ts = (new Date()).getTime();
991+
const sevenDays = 7 * 24 * 60 * 60 * 1000;
992+
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
993+
994+
// Aggregations
995+
const sevenDaysMatch = { '$match': {ls: { '$gte': (ts - sevenDays) / 1000}}};
996+
const thirtyDaysMatch = { '$match': {ls: { '$gte': (ts - thirtyDays) / 1000}}};
997+
const rangeProject = { '$project': { 'range': { '$concat': [] }}};
998+
const indexProject = { '$project': {'count': 1, 'index': { '$concat': [] }}};
999+
const groupBy = { '$group': { '_id': '$range', count: { '$sum': 1 }}};
1000+
const sort = {'$sort': { 'index': 1}};
1001+
1002+
rangeProject.$project.range.$concat = rangeLabels.map((label, index) => {
1003+
const range = ranges[index];
1004+
if (index < 2 && range.length === 1) {
1005+
return { '$cond': [{ '$eq': ['$sc', range[0] ]}, label, '']};
1006+
}
1007+
else if (range.length === 1) {
1008+
return { $cond: [{ $gte: ['$sc', range[0] ]}, label, '']};
1009+
}
1010+
else {
1011+
return { $cond: [{ $and: [{$gte: ['$sc', range[0]]}, {$lte: ['$sc', range[1]]}]}, label, '']};
1012+
}
1013+
});
1014+
1015+
indexProject.$project.index.$concat = rangeLabels.map((label, index) => (
1016+
{ '$cond': [{'$eq': ['$_id', label]}, index.toString(), '']}
1017+
));
1018+
1019+
1020+
// Promises
1021+
const allDataPromise = getAggregatedAppUsers(collectionName, [rangeProject, groupBy, indexProject, sort]);
1022+
const sevenDaysPromise = getAggregatedAppUsers(collectionName, [sevenDaysMatch, rangeProject, groupBy, indexProject, sort]);
1023+
const thirtyDaysPromise = getAggregatedAppUsers(collectionName, [thirtyDaysMatch, rangeProject, groupBy, indexProject, sort]);
1024+
1025+
Promise.all([allDataPromise, sevenDaysPromise, thirtyDaysPromise]).then(promiseResults => {
1026+
common.returnOutput(params, {
1027+
all: promiseResults[0],
1028+
['7days']: promiseResults[1],
1029+
['30days']: promiseResults[2]
1030+
});
1031+
}).catch(() => {
1032+
common.returnOutput(params, {});
1033+
});
1034+
1035+
return true;
1036+
};
1037+
9661038
plugins.extendModule("app_users", usersApi);
9671039

9681040
module.exports = usersApi;

api/utils/requestProcessor.js

+8
Original file line numberDiff line numberDiff line change
@@ -1172,6 +1172,14 @@ const processRequest = (params) => {
11721172
}
11731173
case '/o/app_users': {
11741174
switch (paths[3]) {
1175+
case 'loyalty': {
1176+
if (!params.qstring.app_id) {
1177+
common.returnMessage(params, 400, 'Missing parameter "app_id"');
1178+
return false;
1179+
}
1180+
validateUserForMgmtReadAPI(countlyApi.mgmt.appUsers.loyalty, params);
1181+
break;
1182+
}
11751183
case 'download': {
11761184
if (paths[4] && paths[4] !== '') {
11771185
validateUserForRead(params, function() {

frontend/express/public/javascripts/countly/countly.views.js

+155-18
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,108 @@ window.UserView = countlyView.extend({
188188

189189
window.LoyaltyView = countlyView.extend({
190190
beforeRender: function() {
191-
return $.when(countlySession.initialize()).then(function() {});
191+
return $.when(countlySession.initialize(), this.getLoyaltyData()).then(function() {});
192+
},
193+
getLoyaltyData: function() {
194+
var self = this;
195+
return $.ajax({
196+
type: "GET",
197+
url: countlyCommon.API_PARTS.data.r + '/app_users/loyalty',
198+
data: {
199+
app_id: countlyCommon.ACTIVE_APP_ID,
200+
api_key: countlyGlobal.member.api_key
201+
},
202+
dataType: "json",
203+
success: function(result) {
204+
self.loyaltyData = result;
205+
}
206+
});
207+
},
208+
fetchResult: function() {
209+
var dp = [
210+
{ "data": [[-1, null]], label: jQuery.i18n.map['user-loyalty.all']},
211+
{ "data": [[-1, null]], label: jQuery.i18n.map['user-loyalty.thirty-days']},
212+
{ "data": [[-1, null]], label: jQuery.i18n.map['user-loyalty.seven-days']}
213+
];
214+
var ticks = [[-1, ""]];
215+
var ranges = countlySession.getLoyalityRange();
216+
217+
var allData = this.loyaltyData.all || [];
218+
var thirtyDaysData = this.loyaltyData['30days'] || [];
219+
var sevenDaysData = this.loyaltyData['7days'] || [];
220+
221+
// Chart data
222+
var totals = [0, 0, 0]; //[allTotal, thirtDaysTotal, sevendaysTotal]
223+
224+
for (var iRange = 0; iRange < ranges.length; iRange++) {
225+
var index = ticks.length - 1;
226+
var dp0 = allData.find(function(data) { // eslint-disable-line no-loop-func
227+
return data._id.replace('&gt;', '>') === ranges[iRange];
228+
});
229+
var dp1 = thirtyDaysData.find(function(data) { // eslint-disable-line no-loop-func
230+
return data._id.replace('&gt;', '>') === ranges[iRange];
231+
});
232+
var dp2 = sevenDaysData.find(function(data) { // eslint-disable-line no-loop-func
233+
return data._id.replace('&gt;', '>') === ranges[iRange];
234+
});
235+
236+
if (dp0) {
237+
dp[0].data.push([index, dp0.count]);
238+
totals[0] += dp0.count;
239+
}
240+
241+
if (dp1) {
242+
dp[1].data.push([index, dp1.count]);
243+
totals[1] += dp1.count;
244+
}
245+
246+
if (dp2) {
247+
dp[2].data.push([index, dp2.count]);
248+
totals[2] += dp2.count;
249+
}
250+
251+
if (dp0 || dp1 || dp2) {
252+
ticks.push([index, ranges[iRange]]);
253+
}
254+
}
255+
ticks.push([ticks.length - 1, ""]);
256+
257+
dp[0].data.push([dp[0].data.length - 1, null]);
258+
dp[1].data.push([dp[1].data.length - 1, null]);
259+
dp[2].data.push([dp[2].data.length - 1, null]);
260+
261+
var chartDP = {
262+
dp: dp,
263+
ticks: ticks
264+
};
265+
266+
// Datatable data
267+
var chartData = [];
268+
for (var iTick = 1; iTick < ticks.length - 1; iTick++) {
269+
var all = dp[0].data[iTick][1] ? countlyCommon.formatNumber(dp[0].data[iTick][1]) : 0;
270+
var allPercentage = countlyCommon.formatNumber((100 * all) / totals[0], 2);
271+
272+
var tDays = dp[1].data[iTick][1] ? countlyCommon.formatNumber(dp[1].data[iTick][1]) : 0;
273+
var tDaysPercentage = countlyCommon.formatNumber((100 * tDays) / totals[1], 2);
274+
275+
var sDays = dp[2].data[iTick][1] ? countlyCommon.formatNumber(dp[2].data[iTick][1]) : 0;
276+
var sDaysPercentage = countlyCommon.formatNumber((100 * sDays) / totals[2], 2);
277+
278+
chartData.push({
279+
l: ticks[iTick][1],
280+
a: "<div style='float:left;min-width: 40px'>" + all + "</div><div class='percent-bar' style='width:" + (allPercentage * 0.8) + "%'></div>" + allPercentage + "%",
281+
td: "<div style='float:left;min-width: 40px'>" + tDays + "</div><div class='percent-bar' style='width:" + (tDaysPercentage * 0.8) + "%'></div>" + tDaysPercentage + "%",
282+
sd: "<div style='float:left;min-width: 40px'>" + sDays + "</div><div class='percent-bar' style='width:" + (sDaysPercentage * 0.8) + "%'></div>" + sDaysPercentage + "%"
283+
});
284+
}
285+
286+
return {
287+
chartData: chartData,
288+
chartDP: chartDP
289+
};
192290
},
193291
renderCommon: function(isRefresh) {
194-
var loyaltyData = countlySession.getRangeData("l", "l-ranges", countlySession.explainLoyaltyRange, countlySession.getLoyalityRange());
292+
var chartData = this.fetchResult();
195293

196294
this.templateData = {
197295
"page-title": jQuery.i18n.map["user-loyalty.title"],
@@ -201,23 +299,35 @@ window.LoyaltyView = countlyView.extend({
201299
};
202300

203301
if (!isRefresh) {
302+
var self = this;
303+
204304
$(this.el).html(this.template(this.templateData));
305+
$('#date-selector').hide();
306+
307+
var labelsHtml = $('<div id="label-container"><div class="labels"></div></div>');
308+
var onLabelClick = function() {
309+
$(this).toggleClass("hidden");
310+
countlyCommon.drawGraph(self.getActiveLabelData(chartData.chartDP), "#dashboard-graph", "bar", { legend: { show: false }});
311+
};
312+
for (var i = 0; i < chartData.chartDP.dp.length; i++) {
313+
var data = chartData.chartDP.dp[i];
314+
var labelDOM = $("<div class='label' style='max-width:250px'><div class='color' style='background-color:" + countlyCommon.GRAPH_COLORS[i] + "'></div><div style='max-width:200px' class='text' title='" + data.label + "'>" + data.label + "</div></div>");
315+
labelDOM.on('click', onLabelClick.bind(labelDOM, data));
316+
labelsHtml.find('.labels').append(labelDOM);
317+
}
205318

206-
countlyCommon.drawGraph(loyaltyData.chartDP, "#dashboard-graph", "bar");
319+
$('.widget-content').css('height', '350px');
320+
$('#dashboard-graph').css("height", "85%");
321+
$('#dashboard-graph').after(labelsHtml);
207322

323+
countlyCommon.drawGraph(this.getActiveLabelData(chartData.chartDP), "#dashboard-graph", "bar", { legend: { show: false }});
208324
this.dtable = $('.d-table').dataTable($.extend({}, $.fn.dataTable.defaults, {
209-
"aaData": loyaltyData.chartData,
325+
"aaData": chartData.chartData,
210326
"aoColumns": [
211-
{ "mData": "l", sType: "loyalty", "sTitle": jQuery.i18n.map["user-loyalty.table.session-count"] },
212-
{
213-
"mData": "t",
214-
sType: "formatted-num",
215-
"mRender": function(d) {
216-
return countlyCommon.formatNumber(d);
217-
},
218-
"sTitle": jQuery.i18n.map["common.number-of-users"]
219-
},
220-
{ "mData": "percent", "sType": "percent", "sTitle": jQuery.i18n.map["common.percent"] }
327+
{ "mData": "l", sType: "loyalty", "sTitle": jQuery.i18n.map["user-loyalty.session-count"] },
328+
{ "mData": "a", "sType": "percent", "sTitle": jQuery.i18n.map["user-loyalty.all"] },
329+
{ "mData": "td", "sType": "percent", "sTitle": jQuery.i18n.map["user-loyalty.thirty-days"] },
330+
{ "mData": "sd", "sType": "percent", "sTitle": jQuery.i18n.map["user-loyalty.seven-days"] }
221331
]
222332
}));
223333

@@ -226,15 +336,42 @@ window.LoyaltyView = countlyView.extend({
226336
},
227337
refresh: function() {
228338
var self = this;
229-
$.when(countlySession.initialize()).then(function() {
339+
$.when(this.getLoyaltyData()).then(function() {
230340
if (app.activeView !== self) {
231341
return false;
232342
}
233343

234-
var loyaltyData = countlySession.getRangeData("l", "l-ranges", countlySession.explainLoyaltyRange, countlySession.getLoyalityRange());
235-
countlyCommon.drawGraph(loyaltyData.chartDP, "#dashboard-graph", "bar");
236-
CountlyHelpers.refreshTable(self.dtable, loyaltyData.chartData);
344+
var chartData = self.fetchResult();
345+
countlyCommon.drawGraph(self.getActiveLabelData(chartData.chartDP), "#dashboard-graph", "bar", { legend: { show: false }});
346+
CountlyHelpers.refreshTable(self.dtable, chartData.chartData);
237347
});
348+
},
349+
getActiveLabelData: function(data) {
350+
var labels = _.pluck(data.dp, "label"),
351+
newData = $.extend(true, [], data),
352+
newLabels = $.extend(true, [], labels);
353+
354+
newData.dp[0].color = '#48A3EB';
355+
newData.dp[1].color = '#FF852B';
356+
newData.dp[2].color = "#00C0B7";
357+
358+
$("#label-container").find(".label").each(function() {
359+
var escapedLabel = _.escape($(this).text().replace(/(?:\r\n|\r|\n)/g, ''));
360+
if ($(this).hasClass("hidden") && newLabels.indexOf(escapedLabel) !== -1) {
361+
delete newLabels[newLabels.indexOf(escapedLabel)];
362+
}
363+
});
364+
365+
newLabels = _.compact(newLabels);
366+
var dpData = newData.dp;
367+
newData.dp = [];
368+
for (var j = 0; j < dpData.length; j++) {
369+
if (newLabels.indexOf(dpData[j].label) >= 0) {
370+
newData.dp.push(dpData[j]);
371+
}
372+
}
373+
374+
return newData;
238375
}
239376
});
240377

frontend/express/public/localization/dashboard/dashboard.properties

+4-6
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,10 @@ users.title = USERS
251251

252252
#user-loyalty
253253
user-loyalty.title = USER LOYALTY
254-
user-loyalty.loyalty = Loyalty
255-
user-loyalty.table.session-count = Session Count
256-
user-loyalty.range.first-session = First session
257-
user-loyalty.range.hours = hours
258-
user-loyalty.range.day = day
259-
user-loyalty.range.days = days
254+
user-loyalty.all = All Users
255+
user-loyalty.thirty-days = Active Users(30 days)
256+
user-loyalty.seven-days = Active Users(7 days)
257+
user-loyalty.session-count = Session Count (All Time)
260258

261259
#sessions
262260
sessions.title = SESSIONS

0 commit comments

Comments
 (0)