zulip/static/js/stats/stats.js

1060 lines
36 KiB
JavaScript
Raw Normal View History

import $ from "jquery";
import PlotlyBar from "plotly.js/lib/bar";
import Plotly from "plotly.js/lib/core";
import PlotlyPie from "plotly.js/lib/pie";
import tippy from "tippy.js";
import {$t, $t_html} from "../i18n";
import {page_params} from "../page_params";
Plotly.register([PlotlyBar, PlotlyPie]);
const font_14pt = {
family: "Source Sans 3",
size: 14,
color: "#000000",
};
let last_full_update = Number.POSITIVE_INFINITY;
// TODO: should take a dict of arrays and do it for all keys
function partial_sums(array) {
let accumulator = 0;
return array.map((o) => {
accumulator += o;
return accumulator;
});
}
// Assumes date is a round number of hours
function floor_to_local_day(date) {
const 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) {
const date_copy = floor_to_local_day(date);
date_copy.setHours(-24 * date.getDay());
return date_copy;
}
function format_date(date, include_hour) {
const months = [
$t({defaultMessage: "January"}),
$t({defaultMessage: "February"}),
$t({defaultMessage: "March"}),
$t({defaultMessage: "April"}),
$t({defaultMessage: "May"}),
$t({defaultMessage: "June"}),
$t({defaultMessage: "July"}),
$t({defaultMessage: "August"}),
$t({defaultMessage: "September"}),
$t({defaultMessage: "October"}),
$t({defaultMessage: "November"}),
$t({defaultMessage: "December"}),
];
const month_str = months[date.getMonth()];
const year = date.getFullYear();
const day = date.getDate();
if (include_hour) {
const hour = date.getHours();
const str = hour >= 12 ? "PM" : "AM";
return month_str + " " + day + ", " + (hour % 12) + ":00" + str;
}
return month_str + " " + day + ", " + year;
}
function update_last_full_update(end_times) {
if (end_times.length === 0) {
return;
}
last_full_update = Math.min(last_full_update, end_times.at(-1));
const update_time = new Date(last_full_update * 1000);
const locale_date = update_time.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
const locale_time = update_time.toLocaleTimeString(undefined, {
hour: "numeric",
minute: "numeric",
});
$("#id_last_full_update").text(locale_time + " on " + locale_date);
$("#id_last_full_update").closest(".last-update").show();
}
$(() => {
tippy(".last_update_tooltip", {
// Same defaults as set in our tippyjs module.
maxWidth: 300,
delay: [100, 20],
animation: false,
touch: ["hold", 750],
placement: "top",
});
// Add configuration for any additional tooltips here.
});
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) {
const text = dates.map((date) => date_formatter(date));
const common = {x: dates, type, hoverinfo: "none", text, textposition: "none"};
return {
human: {
// 5062a0
name: $t({defaultMessage: "Humans"}),
y: values.human,
marker: {color: "#5f6ea0"},
...common,
},
bot: {
// a09b5f bbb56e
name: $t({defaultMessage: "Bots"}),
y: values.bot,
marker: {color: "#b7b867"},
...common,
},
me: {
name: $t({defaultMessage: "Me"}),
y: values.me,
marker: {color: "#be6d68"},
...common,
},
};
}
const 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.62,
y: 1.12,
orientation: "h",
font: font_14pt,
},
font: font_14pt,
};
function make_rangeselector(x, y, button1, button2) {
return {
x,
y,
buttons: [
{stepmode: "backward", ...button1},
{stepmode: "backward", ...button2},
{step: "all", label: $t({defaultMessage: "All time"})},
],
};
}
// This is also the cumulative rangeselector
const daily_rangeselector = make_rangeselector(
0.68,
-0.62,
{count: 10, label: $t({defaultMessage: "Last 10 days"}), step: "day"},
{count: 30, label: $t({defaultMessage: "Last 30 days"}), step: "day"},
);
const weekly_rangeselector = make_rangeselector(
0.656,
-0.62,
{count: 2, label: $t({defaultMessage: "Last 2 months"}), step: "month"},
{count: 6, label: $t({defaultMessage: "Last 6 months"}), step: "month"},
);
function add_hover_handler() {
document.querySelector("#id_messages_sent_over_time").on("plotly_hover", (data) => {
$("#hoverinfo").show();
document.querySelector("#hover_date").textContent =
data.points[0].data.text[data.points[0].pointNumber];
const values = [null, null, null];
for (const trace of data.points) {
values[trace.curveNumber] = trace.y;
}
const hover_text_ids = ["#hover_me", "#hover_human", "#hover_bot"];
const hover_value_ids = ["#hover_me_value", "#hover_human_value", "#hover_bot_value"];
for (const [i, value] of values.entries()) {
if (value !== null) {
document.querySelector(hover_text_ids[i]).style.display = "inline";
document.querySelector(hover_value_ids[i]).style.display = "inline";
document.querySelector(hover_value_ids[i]).textContent = value;
} else {
document.querySelector(hover_text_ids[i]).style.display = "none";
document.querySelector(hover_value_ids[i]).style.display = "none";
}
}
});
}
const start_dates = data.end_times.map(
(timestamp) =>
// data.end_times are the ends of hour long intervals.
new Date(timestamp * 1000 - 60 * 60 * 1000),
);
function aggregate_data(aggregation) {
let start;
let 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;
};
}
const dates = [start];
const values = {human: [], bot: [], me: []};
let current = {human: 0, bot: 0, me: 0};
let i_init = 0;
if (is_boundary(start_dates[0])) {
current = {
human: data.everyone.human[0],
bot: data.everyone.bot[0],
me: data.user.human[0],
};
i_init = 1;
}
for (let 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);
values.me.push(current.me);
current = {human: 0, bot: 0, me: 0};
}
current.human += data.everyone.human[i];
current.bot += data.everyone.bot[i];
current.me += data.user.human[i];
}
values.human.push(current.human);
values.bot.push(current.bot);
values.me.push(current.me);
return {
dates,
values,
last_value_is_partial: !is_boundary(
new Date(start_dates.at(-1).getTime() + 60 * 60 * 1000),
),
};
}
// Generate traces
let date_formatter = function (date) {
return format_date(date, true);
};
let values = {me: data.user.human, human: data.everyone.human, bot: data.everyone.bot};
let info = aggregate_data("day");
date_formatter = function (date) {
return format_date(date, false);
};
const last_day_is_partial = info.last_value_is_partial;
const daily_traces = make_traces(info.dates, info.values, "bar", date_formatter);
info = aggregate_data("week");
date_formatter = function (date) {
return $t({defaultMessage: "Week of {date}"}, {date: format_date(date, false)});
};
const last_week_is_partial = info.last_value_is_partial;
const weekly_traces = make_traces(info.dates, info.values, "bar", date_formatter);
const dates = data.end_times.map((timestamp) => new Date(timestamp * 1000));
values = {
human: partial_sums(data.everyone.human),
bot: partial_sums(data.everyone.bot),
me: partial_sums(data.user.human),
};
date_formatter = function (date) {
return format_date(date, true);
};
const 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)
let clicked_cumulative = false;
function draw_or_update_plot(rangeselector, traces, last_value_is_partial, initial_draw) {
$("#daily_button, #weekly_button, #cumulative_button").removeClass("selected");
$("#id_messages_sent_over_time > div").removeClass("spinner");
if (initial_draw) {
traces.human.visible = true;
traces.bot.visible = "legendonly";
traces.me.visible = "legendonly";
} else {
const plotDiv = document.querySelector("#id_messages_sent_over_time");
traces.me.visible = plotDiv.data[0].visible;
traces.human.visible = plotDiv.data[1].visible;
traces.bot.visible = plotDiv.data[2].visible;
}
layout.xaxis.rangeselector = rangeselector;
if (clicked_cumulative || initial_draw) {
Plotly.newPlot(
"id_messages_sent_over_time",
[traces.me, traces.human, traces.bot],
layout,
{displayModeBar: false},
);
add_hover_handler();
} else {
Plotly.deleteTraces("id_messages_sent_over_time", [0, 1, 2]);
Plotly.addTraces("id_messages_sent_over_time", [traces.me, 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
$("#daily_button").on("click", function () {
draw_or_update_plot(daily_rangeselector, daily_traces, last_day_is_partial, false);
$(this).addClass("selected");
clicked_cumulative = false;
});
$("#weekly_button").on("click", function () {
draw_or_update_plot(weekly_rangeselector, weekly_traces, last_week_is_partial, false);
$(this).addClass("selected");
clicked_cumulative = false;
});
$("#cumulative_button").on("click", function () {
clicked_cumulative = false;
draw_or_update_plot(daily_rangeselector, cumulative_traces, false, false);
$(this).addClass("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").addClass("selected");
} else {
draw_or_update_plot(weekly_rangeselector, weekly_traces, last_week_is_partial, true);
$("#weekly_button").addClass("selected");
}
}
function round_to_percentages(values, total) {
return values.map((x) => {
if (x === total) {
return "100%";
}
if (x === 0) {
return "0%";
}
const unrounded = (x / total) * 100;
const precision = Math.min(
6, // this is the max precision (two #, 4 decimal points; 99.9999%).
Math.max(
2, // the minimum amount of precision (40% or 6.0%).
Math.floor(-Math.log10(100 - unrounded)) + 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_) {
const data = new Map();
for (const [key, array] of Object.entries(time_series_data)) {
if (array.length < num_steps) {
num_steps = array.length;
}
let sum = 0;
for (let i = 1; i <= num_steps; i += 1) {
sum += array.at(-i);
}
data.set(key, sum);
}
const labels = labels_.slice();
const values = [];
for (const label of labels) {
if (data.has(label)) {
values.push(data.get(label));
data.delete(label);
} else {
values.push(0);
}
}
if (data.size !== 0) {
labels[labels.length - 1] = "Other";
for (const sum of data.values()) {
values[labels.length - 1] += sum;
}
}
let total = 0;
for (const value of values) {
total += value;
}
return {
values,
labels,
percentages: round_to_percentages(values, total),
total,
};
}
function populate_messages_sent_by_client(data) {
const 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
const everyone_month = compute_summary_chart_data(
data.everyone,
30,
data.display_order.slice(0, 12),
);
const label_values = [];
for (let i = 0; i < everyone_month.values.length; i += 1) {
label_values.push({
label: everyone_month.labels[i],
value: everyone_month.labels[i] === "Other" ? -1 : everyone_month.values[i],
});
}
label_values.sort((a, b) => b.value - a.value);
const labels = [];
for (const item of label_values) {
labels.push(item.label);
}
function make_plot_data(time_series_data, num_steps) {
const plot_data = compute_summary_chart_data(time_series_data, num_steps, labels);
plot_data.values.reverse();
plot_data.labels.reverse();
plot_data.percentages.reverse();
const annotations = {values: [], labels: [], text: []};
for (let 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: "Source Sans 3", size: 18, color: "#000000"},
},
trace_annotations: {
x: annotations.values,
y: annotations.labels,
mode: "text",
type: "scatter",
textposition: "middle right",
text: annotations.text,
},
};
}
const plot_data = {
everyone: {
cumulative: make_plot_data(data.everyone, data.end_times.length),
year: make_plot_data(data.everyone, 365),
month: make_plot_data(data.everyone, 30),
week: make_plot_data(data.everyone, 7),
},
user: {
cumulative: make_plot_data(data.user, data.end_times.length),
year: make_plot_data(data.user, 365),
month: make_plot_data(data.user, 30),
week: make_plot_data(data.user, 7),
},
};
let user_button = "everyone";
let time_button;
if (data.end_times.length >= 30) {
time_button = "month";
$("#messages_by_client_last_month_button").addClass("selected");
} else {
time_button = "cumulative";
$("#messages_by_client_cumulative_button").addClass("selected");
}
if (data.end_times.length < 365) {
$("#pie_messages_sent_by_client button[data-time='year']").remove();
if (data.end_times.length < 30) {
$("#pie_messages_sent_by_client button[data-time='month']").remove();
if (data.end_times.length < 7) {
$("#pie_messages_sent_by_client button[data-time='week']").remove();
}
}
}
function draw_plot() {
$("#id_messages_sent_by_client > div").removeClass("spinner");
const data_ = plot_data[user_button][time_button];
layout.height = layout.margin.b + data_.trace.x.length * 30;
layout.xaxis.range = [0, Math.max(...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) {
$("#pie_messages_sent_by_client button[data-user]").removeClass("selected");
$button.addClass("selected");
}
function set_time_button($button) {
$("#pie_messages_sent_by_client button[data-time]").removeClass("selected");
$button.addClass("selected");
}
$("#pie_messages_sent_by_client button").on("click", function () {
if ($(this).attr("data-user")) {
set_user_button($(this));
user_button = $(this).attr("data-user");
}
if ($(this).attr("data-time")) {
set_time_button($(this));
time_button = $(this).attr("data-time");
}
draw_plot();
});
}
function populate_messages_sent_by_message_type(data) {
const layout = {
margin: {l: 90, r: 0, b: 0, t: 0},
width: 750,
height: 300,
font: font_14pt,
};
function make_plot_data(time_series_data, num_steps) {
const plot_data = compute_summary_chart_data(
time_series_data,
num_steps,
data.display_order,
);
const labels = [];
for (let i = 0; i < plot_data.labels.length; i += 1) {
labels.push(plot_data.labels[i] + " (" + plot_data.percentages[i] + ")");
}
const total_string = plot_data.total.toLocaleString();
return {
trace: {
values: plot_data.values,
labels,
type: "pie",
direction: "clockwise",
rotation: -90,
sort: false,
textinfo: "text",
text: plot_data.labels.map(() => ""),
hoverinfo: "label+value",
pull: 0.05,
marker: {
colors: ["#68537c", "#be6d68", "#b3b348"],
},
},
total_html: $t_html(
{defaultMessage: "<b>Total messages</b>: {total_messages}"},
{total_messages: total_string},
),
};
}
const plot_data = {
everyone: {
cumulative: make_plot_data(data.everyone, data.end_times.length),
year: make_plot_data(data.everyone, 365),
month: make_plot_data(data.everyone, 30),
week: make_plot_data(data.everyone, 7),
},
user: {
cumulative: make_plot_data(data.user, data.end_times.length),
year: make_plot_data(data.user, 365),
month: make_plot_data(data.user, 30),
week: make_plot_data(data.user, 7),
},
};
let user_button = "everyone";
let time_button;
if (data.end_times.length >= 30) {
time_button = "month";
$("#messages_by_type_last_month_button").addClass("selected");
} else {
time_button = "cumulative";
$("#messages_by_type_cumulative_button").addClass("selected");
}
const totaldiv = document.querySelector("#pie_messages_sent_by_type_total");
if (data.end_times.length < 365) {
$("#pie_messages_sent_by_type button[data-time='year']").remove();
if (data.end_times.length < 30) {
$("#pie_messages_sent_by_type button[data-time='month']").remove();
if (data.end_times.length < 7) {
$("#pie_messages_sent_by_type button[data-time='week']").remove();
}
}
}
function draw_plot() {
$("#id_messages_sent_by_message_type > div").removeClass("spinner");
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_html;
}
draw_plot();
// Click handlers
function set_user_button($button) {
$("#pie_messages_sent_by_type button[data-user]").removeClass("selected");
$button.addClass("selected");
}
function set_time_button($button) {
$("#pie_messages_sent_by_type button[data-time]").removeClass("selected");
$button.addClass("selected");
}
$("#pie_messages_sent_by_type button").on("click", function () {
if ($(this).attr("data-user")) {
set_user_button($(this));
user_button = $(this).attr("data-user");
}
if ($(this).attr("data-time")) {
set_time_button($(this));
time_button = $(this).attr("data-time");
}
draw_plot();
});
}
function populate_number_of_users(data) {
const layout = {
width: 750,
height: 370,
margin: {l: 40, r: 0, b: 65, t: 20},
xaxis: {
fixedrange: true,
rangeslider: {bordercolor: "#D8D8D8", borderwidth: 1},
rangeselector: {
x: 0.64,
y: -0.79,
buttons: [
{
count: 2,
label: $t({defaultMessage: "Last 2 months"}),
step: "month",
stepmode: "backward",
},
{
count: 6,
label: $t({defaultMessage: "Last 6 months"}),
step: "month",
stepmode: "backward",
},
{step: "all", label: $t({defaultMessage: "All time"})},
],
},
},
yaxis: {fixedrange: true, rangemode: "tozero"},
font: font_14pt,
};
const end_dates = data.end_times.map((timestamp) => new Date(timestamp * 1000));
const text = end_dates.map((date) => format_date(date, false));
function make_traces(values, type) {
return {
x: end_dates,
y: values,
type,
name: $t({defaultMessage: "Active users"}),
hoverinfo: "none",
text,
visible: true,
};
}
function add_hover_handler() {
document.querySelector("#id_number_of_users").on("plotly_hover", (data) => {
$("#users_hover_info").show();
document.querySelector("#users_hover_date").textContent =
data.points[0].data.text[data.points[0].pointNumber];
const values = [null, null, null];
for (const trace of data.points) {
values[trace.curveNumber] = trace.y;
}
const hover_value_ids = [
"#users_hover_1day_value",
"#users_hover_15day_value",
"#users_hover_all_time_value",
];
for (const [i, value] of values.entries()) {
if (value !== null) {
document.querySelector(hover_value_ids[i]).style.display = "inline";
document.querySelector(hover_value_ids[i]).textContent = value;
} else {
document.querySelector(hover_value_ids[i]).style.display = "none";
}
}
});
}
const _1day_trace = make_traces(data.everyone._1day, "bar");
const _15day_trace = make_traces(data.everyone._15day, "scatter");
const all_time_trace = make_traces(data.everyone.all_time, "scatter");
$("#id_number_of_users > div").removeClass("spinner");
// Redraw the plot every time for simplicity. If we have perf problems with this in the
// future, we can copy the update behavior from populate_messages_sent_over_time
function draw_or_update_plot(trace) {
$("#1day_actives_button, #15day_actives_button, #all_time_actives_button").removeClass(
"selected",
);
Plotly.newPlot("id_number_of_users", [trace], layout, {displayModeBar: false});
add_hover_handler();
}
$("#1day_actives_button").on("click", function () {
draw_or_update_plot(_1day_trace);
$(this).addClass("selected");
});
$("#15day_actives_button").on("click", function () {
draw_or_update_plot(_15day_trace);
$(this).addClass("selected");
});
$("#all_time_actives_button").on("click", function () {
draw_or_update_plot(all_time_trace);
$(this).addClass("selected");
});
// Initial drawing of plot
draw_or_update_plot(all_time_trace, true);
$("#all_time_actives_button").addClass("selected");
}
function populate_messages_read_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) {
const text = dates.map((date) => date_formatter(date));
const common = {x: dates, type, hoverinfo: "none", text, textposition: "none"};
return {
everyone: {
name: $t({defaultMessage: "Everyone"}),
y: values.everyone,
marker: {color: "#5f6ea0"},
...common,
},
me: {
name: $t({defaultMessage: "Me"}),
y: values.me,
marker: {color: "#be6d68"},
...common,
},
};
}
const 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.62,
y: 1.12,
orientation: "h",
font: font_14pt,
},
font: font_14pt,
};
function make_rangeselector(x, y, button1, button2) {
return {
x,
y,
buttons: [
{stepmode: "backward", ...button1},
{stepmode: "backward", ...button2},
{step: "all", label: $t({defaultMessage: "All time"})},
],
};
}
// This is also the cumulative rangeselector
const daily_rangeselector = make_rangeselector(
0.68,
-0.62,
{count: 10, label: $t({defaultMessage: "Last 10 days"}), step: "day"},
{count: 30, label: $t({defaultMessage: "Last 30 days"}), step: "day"},
);
const weekly_rangeselector = make_rangeselector(
0.656,
-0.62,
{count: 2, label: $t({defaultMessage: "Last 2 months"}), step: "month"},
{count: 6, label: $t({defaultMessage: "Last 6 months"}), step: "month"},
);
function add_hover_handler() {
document.querySelector("#id_messages_read_over_time").on("plotly_hover", (data) => {
$("#read_hover_info").show();
document.querySelector("#read_hover_date").textContent =
data.points[0].data.text[data.points[0].pointNumber];
const values = [null, null];
for (const trace of data.points) {
values[trace.curveNumber] = trace.y;
}
const read_hover_text_ids = ["#read_hover_me", "#read_hover_everyone"];
const read_hover_value_ids = ["#read_hover_me_value", "#read_hover_everyone_value"];
for (const [i, value] of values.entries()) {
if (value !== null) {
document.querySelector(read_hover_text_ids[i]).style.display = "inline";
document.querySelector(read_hover_value_ids[i]).style.display = "inline";
document.querySelector(read_hover_value_ids[i]).textContent = value;
} else {
document.querySelector(read_hover_text_ids[i]).style.display = "none";
document.querySelector(read_hover_value_ids[i]).style.display = "none";
}
}
});
}
const start_dates = data.end_times.map(
(timestamp) =>
// data.end_times are the ends of hour long intervals.
new Date(timestamp * 1000 - 60 * 60 * 1000),
);
function aggregate_data(aggregation) {
let start;
let 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;
};
}
const dates = [start];
const values = {everyone: [], me: []};
let current = {everyone: 0, me: 0};
let i_init = 0;
if (is_boundary(start_dates[0])) {
current = {everyone: data.everyone.read[0], me: data.user.read[0]};
i_init = 1;
}
for (let i = i_init; i < start_dates.length; i += 1) {
if (is_boundary(start_dates[i])) {
dates.push(start_dates[i]);
values.everyone.push(current.everyone);
values.me.push(current.me);
current = {everyone: 0, me: 0};
}
current.everyone += data.everyone.read[i];
current.me += data.user.read[i];
}
values.everyone.push(current.everyone);
values.me.push(current.me);
return {
dates,
values,
last_value_is_partial: !is_boundary(
new Date(start_dates.at(-1).getTime() + 60 * 60 * 1000),
),
};
}
// Generate traces
let date_formatter = function (date) {
return format_date(date, true);
};
let values = {me: data.user.read, everyone: data.everyone.read};
let info = aggregate_data("day");
date_formatter = function (date) {
return format_date(date, false);
};
const last_day_is_partial = info.last_value_is_partial;
const daily_traces = make_traces(info.dates, info.values, "bar", date_formatter);
info = aggregate_data("week");
date_formatter = function (date) {
return $t({defaultMessage: "Week of {date}"}, {date: format_date(date, false)});
};
const last_week_is_partial = info.last_value_is_partial;
const weekly_traces = make_traces(info.dates, info.values, "bar", date_formatter);
const dates = data.end_times.map((timestamp) => new Date(timestamp * 1000));
values = {everyone: partial_sums(data.everyone.read), me: partial_sums(data.user.read)};
date_formatter = function (date) {
return format_date(date, true);
};
const 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)
let clicked_cumulative = false;
function draw_or_update_plot(rangeselector, traces, last_value_is_partial, initial_draw) {
$("#read_daily_button, #read_weekly_button, #read_cumulative_button").removeClass(
"selected",
);
$("#id_messages_read_over_time > div").removeClass("spinner");
if (initial_draw) {
traces.everyone.visible = true;
traces.me.visible = "legendonly";
} else {
const plotDiv = document.querySelector("#id_messages_read_over_time");
traces.me.visible = plotDiv.data[0].visible;
traces.everyone.visible = plotDiv.data[1].visible;
}
layout.xaxis.rangeselector = rangeselector;
if (clicked_cumulative || initial_draw) {
Plotly.newPlot("id_messages_read_over_time", [traces.me, traces.everyone], layout, {
displayModeBar: false,
});
add_hover_handler();
} else {
Plotly.deleteTraces("id_messages_read_over_time", [0, 1]);
Plotly.addTraces("id_messages_read_over_time", [traces.me, traces.everyone]);
Plotly.relayout("id_messages_read_over_time", layout);
}
$("#id_messages_read_over_time").attr("last_value_is_partial", last_value_is_partial);
}
// Click handlers for aggregation buttons
$("#read_daily_button").on("click", function () {
draw_or_update_plot(daily_rangeselector, daily_traces, last_day_is_partial, false);
$(this).addClass("selected");
clicked_cumulative = false;
});
$("#read_weekly_button").on("click", function () {
draw_or_update_plot(weekly_rangeselector, weekly_traces, last_week_is_partial, false);
$(this).addClass("selected");
clicked_cumulative = false;
});
$("#read_cumulative_button").on("click", function () {
clicked_cumulative = false;
draw_or_update_plot(daily_rangeselector, cumulative_traces, false, false);
$(this).addClass("selected");
clicked_cumulative = true;
});
// Initial drawing of plot
if (weekly_traces.everyone.x.length < 12) {
draw_or_update_plot(daily_rangeselector, daily_traces, last_day_is_partial, true);
$("#read_daily_button").addClass("selected");
} else {
draw_or_update_plot(weekly_rangeselector, weekly_traces, last_week_is_partial, true);
$("#read_weekly_button").addClass("selected");
}
}
function get_chart_data(data, callback) {
$.get({
url: "/json/analytics/chart_data" + page_params.data_url_suffix,
data,
success(data) {
callback(data);
update_last_full_update(data.end_times);
},
error(xhr) {
$("#id_stats_errors").show().text(JSON.parse(xhr.responseText).msg);
},
});
}
get_chart_data(
{chart_name: "messages_sent_over_time", min_length: "10"},
populate_messages_sent_over_time,
);
get_chart_data(
{chart_name: "messages_sent_by_client", min_length: "10"},
populate_messages_sent_by_client,
);
get_chart_data(
{chart_name: "messages_sent_by_message_type", min_length: "10"},
populate_messages_sent_by_message_type,
);
get_chart_data({chart_name: "number_of_humans", min_length: "10"}, populate_number_of_users);
get_chart_data(
{chart_name: "messages_read_over_time", min_length: "10"},
populate_messages_read_over_time,
);