zulip/static/js/stats/stats.js

709 lines
24 KiB
JavaScript

var font_14pt = {
family: 'Humbug',
size: 14,
color: '#000000',
};
var button_selected = '#D8D8D8';
var button_unselected = '#F0F0F0';
// TODO: should take a dict of arrays and do it for all keys
function partial_sums(array) {
var count = 0;
var cumulative = [];
for (var i = 0; i < array.length; i += 1) {
count += array[i];
cumulative[i] = count;
}
return cumulative;
}
// Assumes date is a round number of hours
function floor_to_local_day(date) {
var date_copy = new Date(date.getTime());
date_copy.setHours(0);
return date_copy;
}
// Assumes date is a round number of hours
function floor_to_local_week(date) {
var date_copy = floor_to_local_day(date);
date_copy.setHours(-24 * date.getDay());
return date_copy;
}
function format_date(date, include_hour) {
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
var month_str = months[date.getMonth()];
var year = date.getFullYear();
var day = date.getDate();
if (include_hour) {
var hour = date.getHours();
var hour_str;
if (hour === 0) {
hour_str = '12 AM';
} else if (hour === 12) {
hour_str = '12 PM';
} else if (hour < 12) {
hour_str = hour + ' AM';
} else {
hour_str = (hour-12) + ' PM';
}
return month_str + ' ' + day + ', ' + hour_str;
}
return month_str + ' ' + day + ', ' + year;
}
function populate_messages_sent_over_time(data) {
if (data.end_times.length === 0) {
// TODO: do something nicer here
return;
}
// Helper functions
function make_traces(dates, values, type, date_formatter) {
var text = dates.map(function (date) {
return date_formatter(date);
});
var common = { x: dates, type: type, hoverinfo: 'none', text: text };
return {
human: $.extend({ // 5062a0
name: "Humans", y: values.human, marker: {color: '#5f6ea0'}}, common),
bot: $.extend({ // a09b5f bbb56e
name: "Bots", y: values.bot, marker: {color: '#b7b867'}}, common),
};
}
var layout = {
barmode: 'group',
width: 750,
height: 400,
margin: { l: 40, r: 0, b: 40, t: 0 },
xaxis: {
fixedrange: true,
rangeslider: { bordercolor: '#D8D8D8', borderwidth: 1 },
type: 'date',
},
yaxis: { fixedrange: true, rangemode: 'tozero' },
legend: {
x: 0.75, y: 1.12, orientation: 'h', font: font_14pt,
},
font: font_14pt,
};
function make_rangeselector(x, y, button1, button2) {
return { x: x, y: y,
buttons: [$.extend({stepmode: 'backward'}, button1),
$.extend({stepmode: 'backward'}, button2),
{step: 'all', label: 'All time'}] };
}
var hourly_rangeselector = make_rangeselector(
0.66, -0.62,
{count: 24, label: 'Last 24 Hours', step: 'hour'},
{count: 72, label: 'Last 72 Hours', step: 'hour'});
// This is also the cumulative rangeselector
var daily_rangeselector = make_rangeselector(
0.68, -0.62,
{count: 10, label: 'Last 10 Days', step: 'day'},
{count: 30, label: 'Last 30 Days', step: 'day'});
var weekly_rangeselector = make_rangeselector(
0.656, -0.62,
{count: 2, label: 'Last 2 Months', step: 'month'},
{count: 6, label: 'Last 6 Months', step: 'month'});
function add_hover_handler() {
document.getElementById('id_messages_sent_over_time').on('plotly_hover', function (data) {
document.getElementById('hover_date').innerText =
data.points[0].data.text[data.points[0].pointNumber];
var values = [null, null];
data.points.forEach(function (trace) {
values[trace.curveNumber] = trace.y;
});
if (values[0] !== null) {
document.getElementById('hover_human').style.display = 'inline';
document.getElementById('hover_human_value').style.display = 'inline';
document.getElementById('hover_human_value').innerText = values[0];
} else {
document.getElementById('hover_human').style.display = 'none';
document.getElementById('hover_human_value').style.display = 'none';
}
if (values[1] !== null) {
document.getElementById('hover_bot').style.display = 'inline';
document.getElementById('hover_bot_value').style.display = 'inline';
document.getElementById('hover_bot_value').innerText = values[1];
} else {
document.getElementById('hover_bot').style.display = 'none';
document.getElementById('hover_bot_value').style.display = 'none';
}
});
}
var start_dates = data.end_times.map(function (timestamp) {
// data.end_times are the ends of hour long intervals.
return new Date(timestamp*1000 - 60*60*1000);
});
function aggregate_data(aggregation) {
var start;
var is_boundary;
if (aggregation === 'day') {
start = floor_to_local_day(start_dates[0]);
is_boundary = function (date) {
return date.getHours() === 0;
};
} else if (aggregation === 'week') {
start = floor_to_local_week(start_dates[0]);
is_boundary = function (date) {
return date.getHours() === 0 && date.getDay() === 0;
};
}
var dates = [start];
var values = {human: [], bot: []};
var current = {human: 0, bot: 0};
var i_init = 0;
if (is_boundary(start_dates[0])) {
current = {human: data.realm.human[0], bot: data.realm.bot[0]};
i_init = 1;
}
for (var i = i_init; i < start_dates.length; i += 1) {
if (is_boundary(start_dates[i])) {
dates.push(start_dates[i]);
values.human.push(current.human);
values.bot.push(current.bot);
current = {human: 0, bot: 0};
}
current.human += data.realm.human[i];
current.bot += data.realm.bot[i];
}
values.human.push(current.human);
values.bot.push(current.bot);
return {
dates: dates, values: values,
last_value_is_partial: !is_boundary(new Date(
start_dates[start_dates.length-1].getTime() + 60*60*1000))};
}
// Generate traces
var date_formatter = function (date) {
return format_date(date, true);
};
var hourly_traces = make_traces(start_dates, data.realm, 'bar', date_formatter);
var info = aggregate_data('day');
date_formatter = function (date) {
return format_date(date, false);
};
var last_day_is_partial = info.last_value_is_partial;
var daily_traces = make_traces(info.dates, info.values, 'bar', date_formatter);
info = aggregate_data('week');
date_formatter = function (date) {
// return i18n.t("Week of __date__", {date: format_date(date, false)});
return "Week of " + format_date(date, false);
};
var last_week_is_partial = info.last_value_is_partial;
var weekly_traces = make_traces(info.dates, info.values, 'bar', date_formatter);
var dates = data.end_times.map(function (timestamp) {
return new Date(timestamp*1000);
});
var values = {human: partial_sums(data.realm.human), bot: partial_sums(data.realm.bot)};
date_formatter = function (date) {
return format_date(date, true);
};
var cumulative_traces = make_traces(dates, values, 'scatter', date_formatter);
// Functions to draw and interact with the plot
// We need to redraw plot entirely if switching from (the cumulative) line
// graph to any bar graph, since otherwise the rangeselector shows both (plotly bug)
var clicked_cumulative = false;
function draw_or_update_plot(rangeselector, traces, last_value_is_partial, initial_draw) {
$('#daily_button').css('background', button_unselected);
$('#weekly_button').css('background', button_unselected);
$('#hourly_button').css('background', button_unselected);
$('#cumulative_button').css('background', button_unselected);
if (initial_draw) {
traces.human.visible = true;
traces.bot.visible = 'legendonly';
} else {
var plotDiv = document.getElementById('id_messages_sent_over_time');
traces.human.visible = plotDiv.data[0].visible;
traces.bot.visible = plotDiv.data[1].visible;
}
layout.xaxis.rangeselector = rangeselector;
if (clicked_cumulative || initial_draw) {
Plotly.newPlot('id_messages_sent_over_time',
[traces.human, traces.bot], layout, {displayModeBar: false});
add_hover_handler();
} else {
Plotly.deleteTraces('id_messages_sent_over_time', [0,1]);
Plotly.addTraces('id_messages_sent_over_time', [traces.human, traces.bot]);
Plotly.relayout('id_messages_sent_over_time', layout);
}
$('#id_messages_sent_over_time').attr('last_value_is_partial', last_value_is_partial);
}
// Click handlers for aggregation buttons
$('#hourly_button').click(function () {
draw_or_update_plot(hourly_rangeselector, hourly_traces, false, false);
$(this).css('background', button_selected);
clicked_cumulative = false;
});
$('#daily_button').click(function () {
draw_or_update_plot(daily_rangeselector, daily_traces, last_day_is_partial, false);
$(this).css('background', button_selected);
clicked_cumulative = false;
});
$('#weekly_button').click(function () {
draw_or_update_plot(weekly_rangeselector, weekly_traces, last_week_is_partial, false);
$(this).css('background', button_selected);
clicked_cumulative = false;
});
$('#cumulative_button').click(function () {
clicked_cumulative = false;
draw_or_update_plot(daily_rangeselector, cumulative_traces, false, false);
$(this).css('background', button_selected);
clicked_cumulative = true;
});
// Initial drawing of plot
if (weekly_traces.human.x.length < 12) {
draw_or_update_plot(daily_rangeselector, daily_traces, last_day_is_partial, true);
$('#daily_button').css('background', button_selected);
} else {
draw_or_update_plot(weekly_rangeselector, weekly_traces, last_week_is_partial, true);
$('#weekly_button').css('background', button_selected);
}
}
$.get({
url: '/json/analytics/chart_data',
data: {chart_name: 'messages_sent_over_time', min_length: '10'},
idempotent: true,
success: function (data) {
populate_messages_sent_over_time(data);
},
error: function (xhr) {
$('#id_stats_errors').show().text($.parseJSON(xhr.responseText).msg);
},
});
function round_to_percentages(values, total) {
return values.map(function (x) {
if (x === total) {
return '100%';
}
if (x === 0) {
return '0%';
}
var unrounded = x/total*100;
var precision = Math.min(6, Math.max(2, Math.floor(
-Math.log(100-unrounded)/Math.log(10)) + 3));
return unrounded.toPrecision(precision) + '%';
});
}
// Last label will turn into "Other" if time_series data has a label not in labels
function compute_summary_chart_data(time_series_data, num_steps, labels_) {
var data = {};
var key;
for (key in time_series_data) {
if (time_series_data[key].length < num_steps) {
num_steps = time_series_data[key].length;
}
var sum = 0;
for (var i=1; i<=num_steps; i+=1) {
sum += time_series_data[key][time_series_data[key].length-i];
}
data[key] = sum;
}
var labels = labels_.slice();
var values = [];
labels.forEach(function (label) {
if (data.hasOwnProperty(label)) {
values.push(data[label]);
delete data[label];
} else {
values.push(0);
}
});
if (!$.isEmptyObject(data)) {
labels[labels.length-1] = "Other";
for (key in data) {
if (data.hasOwnProperty(key)) {
values[labels.length-1] += data[key];
}
}
}
var total = values.reduce(function (a, b) { return a + b; }, 0);
return {
values: values,
labels: labels,
percentages: round_to_percentages(values, total),
total: total,
};
}
function populate_messages_sent_by_client(data) {
var layout = {
width: 750,
height: null, // set in draw_plot()
margin: { l: 3, r: 40, b: 40, t: 0 },
font: font_14pt,
xaxis: { range: null }, // set in draw_plot()
yaxis: { showticklabels: false },
showlegend: false,
};
// sort labels so that values are descending in the default view
var realm_cumulative = compute_summary_chart_data(data.realm, data.end_times.length,
data.display_order.slice(0, 12));
var label_values = [];
for (var i=0; i<realm_cumulative.values.length; i+=1) {
label_values.push({
label: realm_cumulative.labels[i],
value: realm_cumulative.labels[i] === "Other" ? -1 : realm_cumulative.values[i],
});
}
label_values.sort(function (a, b) { return b.value - a.value; });
var labels = [];
label_values.forEach(function (item) { labels.push(item.label); });
function make_plot_data(time_series_data, num_steps) {
var plot_data = compute_summary_chart_data(time_series_data, num_steps, labels);
plot_data.values.reverse();
plot_data.labels.reverse();
plot_data.percentages.reverse();
var annotations = { values : [], labels : [], text : []};
for (var i=0; i<plot_data.values.length; i+=1) {
if (plot_data.values[i] > 0) {
annotations.values.push(plot_data.values[i]);
annotations.labels.push(plot_data.labels[i]);
annotations.text.push(' ' + plot_data.labels[i] + ' (' + plot_data.percentages[i] + ')');
}
}
return {
trace: {
x: plot_data.values,
y: plot_data.labels,
type: 'bar',
orientation: 'h',
sort: false,
textinfo: "text",
hoverinfo: "none",
marker: { color: '#537c5e' },
font: { family: 'Humbug', size: 18, color: '#000000' },
},
trace_annotations: {
x: annotations.values,
y: annotations.labels,
mode: 'text',
type: 'scatter',
textposition: 'middle right',
text: annotations.text,
},
};
}
var plot_data = {
realm: {
cumulative: make_plot_data(data.realm, data.end_times.length),
thirty: make_plot_data(data.realm, 30),
ten: make_plot_data(data.realm, 10),
},
user: {
cumulative: make_plot_data(data.user, data.end_times.length),
thirty: make_plot_data(data.user, 30),
ten: make_plot_data(data.user, 10),
},
};
var user_button = 'realm';
var time_button = 'cumulative';
function draw_plot() {
var data_ = plot_data[user_button][time_button];
layout.height = layout.margin.b + data_.trace.x.length * 30;
layout.xaxis.range = [0, Math.max.apply(null, data_.trace.x) * 1.3];
Plotly.newPlot('id_messages_sent_by_client',
[data_.trace, data_.trace_annotations],
layout,
{displayModeBar: false, staticPlot: true});
}
draw_plot();
// Click handlers
function set_user_button(button) {
$('#messages_by_client_realm_button').css('background', button_unselected);
$('#messages_by_client_user_button').css('background', button_unselected);
button.css('background', button_selected);
}
function set_time_button(button) {
$('#messages_by_client_cumulative_button').css('background', button_unselected);
$('#messages_by_client_thirty_days_button').css('background', button_unselected);
$('#messages_by_client_ten_days_button').css('background', button_unselected);
button.css('background', button_selected);
}
$('#messages_by_client_realm_button').click(function () {
set_user_button($(this));
user_button = 'realm';
draw_plot();
});
$('#messages_by_client_user_button').click(function () {
set_user_button($(this));
user_button = 'user';
draw_plot();
});
$('#messages_by_client_cumulative_button').click(function () {
set_time_button($(this));
time_button = 'cumulative';
draw_plot();
});
$('#messages_by_client_thirty_days_button').click(function () {
set_time_button($(this));
time_button = 'thirty';
draw_plot();
});
$('#messages_by_client_ten_days_button').click(function () {
set_time_button($(this));
time_button = 'ten';
draw_plot();
});
// handle links with @href started with '#' only
$(document).on('click', 'a[href^="#"]', function (e) {
// target element id
var id = $(this).attr('href');
// target element
var $id = $(id);
if ($id.length === 0) {
return;
}
// prevent standard hash navigation (avoid blinking in IE)
e.preventDefault();
var pos = $id.offset().top+$('.page-content')[0].scrollTop-50;
$('.page-content').animate({scrollTop: pos + "px"}, 500);
});
}
$.get({
url: '/json/analytics/chart_data',
data: {chart_name: 'messages_sent_by_client', min_length: '10'},
idempotent: true,
success: function (data) {
populate_messages_sent_by_client(data);
},
error: function (xhr) {
$('#id_stats_errors').show().text($.parseJSON(xhr.responseText).msg);
},
});
function populate_messages_sent_by_message_type(data) {
var layout = {
margin: { l: 90, r: 0, b: 0, t: 0 },
width: 550,
height: 300,
font: font_14pt,
};
function make_plot_data(time_series_data, num_steps) {
var plot_data = compute_summary_chart_data(time_series_data, num_steps, data.display_order);
var labels = [];
for (var i=0; i<plot_data.labels.length; i+=1) {
labels.push(plot_data.labels[i] + ' (' + plot_data.percentages[i] + ')');
}
return {
trace: {
values: plot_data.values,
labels: labels,
type: 'pie',
direction: 'clockwise',
rotation: -90,
sort: false,
textinfo: "text",
text: plot_data.labels.map(function () { return ''; }),
hoverinfo: "label+value",
pull: 0.05,
marker: {
colors: ['#68537c', '#be6d68', '#b3b348'],
},
},
total_str: "Total messages: " + plot_data.total.toString().
replace(/\B(?=(\d{3})+(?!\d))/g, ","),
};
}
var plot_data = {
realm: {
cumulative: make_plot_data(data.realm, data.end_times.length),
thirty: make_plot_data(data.realm, 30),
ten: make_plot_data(data.realm, 10),
},
user: {
cumulative: make_plot_data(data.user, data.end_times.length),
thirty: make_plot_data(data.user, 30),
ten: make_plot_data(data.user, 10),
},
};
var user_button = 'realm';
var time_button = 'cumulative';
var totaldiv = document.getElementById('pie_messages_sent_by_type_total');
function draw_plot() {
Plotly.newPlot('id_messages_sent_by_message_type',
[plot_data[user_button][time_button].trace],
layout,
{displayModeBar: false});
totaldiv.innerHTML = plot_data[user_button][time_button].total_str;
}
draw_plot();
// Click handlers
function set_user_button(button) {
$('#messages_by_type_realm_button').css('background', button_unselected);
$('#messages_by_type_user_button').css('background', button_unselected);
button.css('background', button_selected);
}
function set_time_button(button) {
$('#messages_by_type_cumulative_button').css('background', button_unselected);
$('#messages_by_type_thirty_days_button').css('background', button_unselected);
$('#messages_by_type_ten_days_button').css('background', button_unselected);
button.css('background', button_selected);
}
$('#messages_by_type_realm_button').click(function () {
set_user_button($(this));
user_button = 'realm';
draw_plot();
});
$('#messages_by_type_user_button').click(function () {
set_user_button($(this));
user_button = 'user';
draw_plot();
});
$('#messages_by_type_cumulative_button').click(function () {
set_time_button($(this));
time_button = 'cumulative';
draw_plot();
});
$('#messages_by_type_thirty_days_button').click(function () {
set_time_button($(this));
time_button = 'thirty';
draw_plot();
});
$('#messages_by_type_ten_days_button').click(function () {
set_time_button($(this));
time_button = 'ten';
draw_plot();
});
}
$.get({
url: '/json/analytics/chart_data',
data: {chart_name: 'messages_sent_by_message_type', min_length: '10'},
idempotent: true,
success: function (data) {
populate_messages_sent_by_message_type(data);
},
error: function (xhr) {
$('#id_stats_errors').show().text($.parseJSON(xhr.responseText).msg);
},
});
function populate_number_of_users(data) {
var layout = {
width: 750,
height: 370,
margin: {
l: 40, r: 0, b: 100, t: 20,
},
xaxis: {
fixedrange: true,
rangeselector: {
x: 0.808,
y: -0.2,
buttons: [
{
count: 30,
label: 'Last 30 Days',
step: 'day',
stepmode: 'backward',
},
{
step: 'all',
label: 'All time',
},
],
},
},
yaxis: {
fixedrange: true,
rangemode: 'tozero',
},
font: font_14pt,
};
var end_dates = data.end_times.map(function (timestamp) {
return new Date(timestamp*1000);
});
var text = end_dates.map(function (date) {
return format_date(date, false);
});
var trace = {
x: end_dates,
y: data.realm.human,
type: 'scatter',
name: "Active users",
hoverinfo: 'none',
text: text,
visible: true,
};
Plotly.newPlot('id_number_of_users', [trace], layout, {displayModeBar: false});
document.getElementById('id_number_of_users').on('plotly_hover', function (data) {
document.getElementById('users_hover_date').innerText =
data.points[0].data.text[data.points[0].pointNumber];
document.getElementById('users_hover_humans').style.display = 'inline';
document.getElementById('users_hover_humans_value').innerText = data.points[0].y;
});
var value_today = data.realm.human[data.realm.human.length - 1];
document.getElementById('number_of_users_today').innerText =
value_today.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
$.get({
url: '/json/analytics/chart_data',
data: {chart_name: 'number_of_humans', min_length: '10'},
idempotent: true,
success: function (data) {
populate_number_of_users(data);
},
error: function (xhr) {
$('#id_stats_errors').show().text($.parseJSON(xhr.responseText).msg);
},
});